SpringBoot Security是Spring官方提供的一个安全框架,他的核心功能是对系统用户进行认证和鉴权,也经常在项目中被使用到,本文不介绍其太过深入的内容,只介绍如何实现并完成认证和鉴权的测试。主要分三步来实现:
- 配置JWT
- 配置Security
- 编写测试相关代码
首先创建一个springboot项目,我的版本是2.6.13,依然是java8,整合Security+JWT需要用到的Maven依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
配置JWT
- 先在
yml配置文件中添加jwt相关配置jwt: expiration: 3600000 //token过期时间,1个小时 tokenHeader: Authorization //token在header中的属性名 secret: jwt-token-secret //生成token的密钥 - 创建
jwt工具类,方便实现根据用户信息生成token,以及通过token中获取用户信息@Component @Data public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -3301605591108950415L; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; private Clock clock = DefaultClock.INSTANCE; //根据用户信息生成token public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } private String doGenerateToken(Map<String, Object> claims, String subject) { final Date createdDate = clock.now(); final Date expirationDate = calculateExpirationDate(createdDate); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(createdDate) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } private Date calculateExpirationDate(Date createdDate) { return new Date(createdDate.getTime() + expiration); } public Boolean validateToken(String token, UserDetails userDetails) { SecurityUserDetails user = (SecurityUserDetails) userDetails; final String username = getUsernameFromToken(token); return (username.equals(user.getUsername()) && !isTokenExpired(token) ); } //通过token获取用户名username public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(clock.now()); } public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } }
配置Security
-
编写一个存储用户信息的
UserDetails的实现类@Data public class SysUser { private Integer id; private String username; private String password; }@Data @EqualsAndHashCode @Accessors(chain = true) //实现链式set方法 public class SecurityUserDetails extends SysUser implements UserDetails { //权限列表 private Collection<? extends GrantedAuthority> authorities; public SecurityUserDetails(String userName,Collection<? extends GrantedAuthority> authorities){ this.setUsername(userName); String encode = new BCryptPasswordEncoder().encode("123456"); this.setPassword(encode); this.setAuthorities(authorities); } /** * 下面这些都返回true * @return */ @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }提示
因为只是记录一下如何实现security+jwt,所以没有从数据库中读取真实的用户信息,而是直接将用户信息和权限信息写死测试。 -
重写
UserDetailsService的loadUserByUsername方法实现具体的认证授权逻辑@Service public class JwtUserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> authorityList = new ArrayList<>(); authorityList.add(new SimpleGrantedAuthority("ROLE_USER")); return new SecurityUserDetails(username,authorityList); } }提示
这里直接把用户的权限写死,ROLE_USER表示用户拥有USER权限,因为权限都是以ROLE_开头的。 -
紧接着创建一个用户请求的过滤器,用来拦截用户请求,分析用户有没有该请求的权限
@Component public class JwtAuthorizationTokenFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final JwtTokenUtil jwtTokenUtil; private final String tokenHeader; public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsServiceImpl") UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, @Value("${jwt.tokenHeader}") String tokenHeader){ this.userDetailsService = userDetailsService; this.jwtTokenUtil = jwtTokenUtil; this.tokenHeader = tokenHeader; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String requestHeader = request.getHeader(this.tokenHeader); String username = null; String authToken = null; if(requestHeader != null && requestHeader.startsWith("Bearer ")){ authToken = requestHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(authToken); }catch (ExpiredJwtException e){ e.printStackTrace(); } } if(username!=null&& SecurityContextHolder.getContext().getAuthentication() == null){ UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if(jwtTokenUtil.validateToken(authToken,userDetails)){ UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request,response); } }提示
Bearer必须带空格,第二个if判断就是为了加载到用户的信息,并且在Security上下文中存储用户及用户的权限的信息 -
实现
AuthenticationEntryPoint接口的commence方法,当请求没有携带认证信息或者说认证失败时,使用我们自己编写的处理逻辑。@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } }提示
如果请求没有携带认证信息或者说认证失败时,会返回给客户端401,如果不重写commence方法,默认返回403 -
接下来编写
Security的核心配置类,重写WebSecurityConfigurerAdapter中的configure方法@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired JwtUserDetailsServiceImpl jwtUserDetailsService; @Autowired JwtAuthorizationTokenFilter authenticationTokenFilter; @Autowired @Lazy PasswordEncoder passwordEncoder; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").anonymous() .anyRequest().authenticated() //除上面以外的都拦截 .and() .csrf().disable() //禁用security自带的跨域处理 //让Security不使用session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean public PasswordEncoder passwordEncoderBean() { return new BCryptPasswordEncoder(); } /** * 认证逻辑配置 */ @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder); } }提示
上面的代码中,.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)表示使用自定义的认证失败处理逻辑。并且配置类中,自定义了用户密码的加密方式,configureGlobal方法设置自定义的loadUserByUsername方法实现和校验密码校验的加密方式。
编写测试相关代码
- 编写一个不需要认证授权就能访问的登录接口
/login@RestController public class LoginController { @Autowired @Qualifier("jwtUserDetailsServiceImpl") private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @PostMapping("/login") public String login(@RequestBody SysUser sysUser, HttpServletRequest request){ final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); return token; } } - 编写一个需要
USER权限的接口/sys/testUser@RestController @RequestMapping("/sys") public class SysUserController { @PreAuthorize("hasAnyRole('USER')") @PostMapping(value = "/testUser") public String testNeed() { return "hello world"; } }
测试
启动SpringBoot项目,对上面的接口进行测试,首先调用/login接口登录并获取token
请求成功并获取到
jwt生成的token。紧接着调用需要USER权限的/testUser,请求时要在请求头里面携带token
请求成功!
现在来测试一下失败的情况,不传token直接请求
请求失败,返回
401,表示没有认证。再来测试一下如果将@PreAuthorize("hasAnyRole('USER')")中的权限改为Admin,然后用刚刚生成的token去请求
@PreAuthorize("hasAnyRole('Admin')")
@PostMapping(value = "/testUser")
public String testNeed() {
return "hello world";
}
由于token中包含的授权信息是
USER,所以将@PreAuthorize("hasAnyRole('USER')")中的USER改为Admin后,返回了403,表示没有这个权限。