本文将详细介绍在spring boot环境中使用spring security实现token认证的方法和注意事项。
Maven依赖 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.0</version > </dependency >
Spring Security 核心类 要利用Spring Security把安全的事情做了,最核心的就是继承这个WebSecurityConfigurerAdapter
,根据自己的业务需要重新configure
方法,示例代码如下:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Autowired private UserDetailsService userDetailsService; @Value("${anonymous.path}") private String[] ANONYMOUS_PATH; @Autowired public void configureAuthentication (AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .userDetailsService(this .userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder(); } @Bean public JwtAuthenticationTokenFilter authenticationTokenFilterBean () throws Exception { return new JwtAuthenticationTokenFilter(); } @Override public void configure (WebSecurity web) throws Exception { if (ANONYMOUS_PATH.length > 0 ) { web.ignoring().antMatchers(ANONYMOUS_PATH); } } @Override protected void configure (HttpSecurity httpSecurity) throws Exception { httpSecurity .csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers( HttpMethod.GET, "/" , "/*.html" , "/favicon.ico" , "/**/*.html" , "/**/*.css" , "/layui/**" , "/dist/**" , "/**/*.js" ).permitAll() .antMatchers(HttpMethod.OPTIONS, "/**" ).permitAll() .antMatchers( "/auth/login" , "/swagger-ui.html" , "/webjars/**" , "/swagger-resources/**" , "/*/api-docs" , "/configuration/**" , "/attachment/**" , "/system/encrypt" , "/actuator/**" , "/h2/**" ).permitAll() .anyRequest().authenticated(); httpSecurity .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); httpSecurity.headers().cacheControl(); httpSecurity.headers().frameOptions().disable(); } }
configure(HttpSecurity httpSecurity)
里面的注释很全,主要做的事情是配置规则和参数。
configure(WebSecurity web)
的目的是实现自定义的匿名访问授权。
OncePerRequestFilter 上面的配置类中声明了要使用authenticationTokenFilterBean
进行权限验证,下面这个继承OncePerRequestFilter
就是它的具体实现,示例如下:
@SuppressWarnings("SpringJavaAutowiringInspection") @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private JwtUserDetailsService jwtUserDetailsService; @Autowired private UserService userService; private String tokenHeader = "Authorization" ; @Override protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String authToken = request.getHeader(this .tokenHeader); if (authToken != null ) { String usercode = TokenUtils.getUsercodeFromToken(authToken); logger.info("checking authentication " + usercode); if (usercode != null && SecurityContextHolder.getContext().getAuthentication() == null ) { SystemUserModel user = userService.getUserByUsercode(usercode); UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(usercode); if (TokenUtils.validateToken(authToken, userDetails) && userService.isPermissionApi(user.getId(), request.getRequestURI(), request.getMethod())) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null , userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( request)); logger.info("authenticated user " + usercode + ", setting security context" ); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
doFilterInternal
是核心方法,在这里实现了token的提取和认证
UserDetailsService UserDetailsService
是Spring Security获取用户信息的核心类,配合使用的model是UserDetails
,用这个类去存放用户信息。
@Service public class JwtUserDetailsService implements UserDetailsService { private UserService userService; @Override public UserDetails loadUserByUsername (String usercode) throws UsernameNotFoundException { SystemUserModel user = userService.getUserByUsercode(usercode); if (user == null ) { throw new UsernameNotFoundException(String.format("No user found with username '%s'." , usercode)); } else { String role = userService.getRole(usercode).getRolename(); return JwtUserFactory.create(user, role); } } @Autowired public void setUserService (UserService userService) { this .userService = userService; } }
loadUserByUsername
是需要重写的方法,在这里写明如何加载用户
UserDetails 下面是UserDetails的一个示例,要点就是要实现UserDetails里的接口
public class JwtUser implements UserDetails { private final String id; private final String username; private final String password; private final String email; private final Collection<? extends GrantedAuthority> authorities; public JwtUser ( String id, String username, String password, String email, Collection<? extends GrantedAuthority> authorities) { this .id = id; this .username = username; this .password = password; this .email = email; this .authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @JsonIgnore public String getId () { return id; } @JsonIgnore @Override public String getPassword () { return password; } @Override public String getUsername () { return username; } @JsonIgnore @Override public boolean isAccountNonExpired () { return true ; } @JsonIgnore @Override public boolean isAccountNonLocked () { return true ; } @JsonIgnore @Override public boolean isCredentialsNonExpired () { return true ; } @JsonIgnore @Override public boolean isEnabled () { return true ; } }
Token 核心类 token这块的重点就是如何生成token,如何验证token,如何刷新token。这些可以封装成一个工具类。如下:
@Component public class TokenUtils { public static final String CLAIM_KEY_USERNAME = "sub" ; public static final String CLAIM_KEY_CREATED = "crt" ; private static String secret; private static Long expiration; public static String getUsercodeFromToken (String token) { String usercode; try { final Claims claims = getClaimsFromToken(token); usercode = claims.getSubject(); } catch (Exception e) { usercode = e.toString(); } return usercode; } public static Date getCreatedDateFromToken (String token) { Date created; try { final Claims claims = getClaimsFromToken(token); created = Objects.equals(null , claims) ? null : new Date((Long) claims.get(CLAIM_KEY_CREATED)); } catch (Exception e) { created = null ; } return created; } public static Date getExpirationDateFromToken (String token) { Date expiration; try { final Claims claims = getClaimsFromToken(token); expiration = claims.getExpiration(); } catch (Exception e) { expiration = null ; } return expiration; } private static Claims getClaimsFromToken (String token) { Claims claims; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { claims = null ; } return claims; } private static Date generateExpirationDate () { return new Date(System.currentTimeMillis() + expiration * 1000 ); } private static Boolean isTokenExpired (String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } private static Boolean isCreatedBeforeLastPasswordReset (Date created, Date lastPasswordReset) { return (lastPasswordReset != null && created.before(lastPasswordReset)); } public static String generateToken (String usercode) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, usercode); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } public static String generateToken (Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } public static Boolean canTokenBeRefreshed (String token, Date lastPasswordReset) { final Date created = getCreatedDateFromToken(token); return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset) && !isTokenExpired(token); } public static String refreshToken (String token) { String refreshedToken; try { final Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); refreshedToken = generateToken(claims); } catch (Exception e) { refreshedToken = null ; } return refreshedToken; } public static Boolean validateToken (String token,SystemUserModel user) { final String usercode = getUsercodeFromToken(token); return (usercode.equals(user.getUsercode()) && !isTokenExpired(token)); } public static Boolean validateToken (String token,UserDetails userDetails) { final String usercode = getUsercodeFromToken(token); return (usercode.equals(userDetails.getUsername()) && !isTokenExpired(token)); } @Value("${jwt.secret}") public void setSecret (String secret) { TokenUtils.secret = secret; } @Value("${jwt.expiration}") public void setExpiration (Long expiration) { TokenUtils.expiration = expiration; } }
END