Spring Security 访问控制 源码解析
上篇 Spring Security 登录校验 源码解析 分析了使用Spring Security时用户登录时验证并返回token过程,本篇分析下用户带token访问时,如何验证用户登录状态及权限问题
用户访问控制相对简单,本质同登录验证一样,均采用过滤器拦截请求进行验证
这里需要自定义过滤器JwtAuthenticationTokenFilter并自定义路径匹配器RequestMatcher ,JwtAuthenticationTokenFilter继承AbstractAuthenticationProcessingFilter,并实现attemptAuthentication、successfulAuthentication、unsuccessfulAuthentication方法
1 AbstractAuthenticationProcessingFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; //判断访问是否需要过滤 if(!this.requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { if(this.logger.isDebugEnabled()) { this.logger.debug("Request is to process authentication"); } Authentication authResult; //调用子类JwtAuthenticationTokenFilter实现 try { authResult = this.attemptAuthentication(request, response); if(authResult == null) { return; } //session控制策略,因为用jwt,故不进行深入分析 this.sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException var8) { this.logger.error("An internal error occurred while trying to authenticate the user.", var8); //验证失败,调用子类unsuccessfulAuthentication方法 this.unsuccessfulAuthentication(request, response, var8); return; } catch (AuthenticationException var9) { //验证失败,调用子类unsuccessfulAuthentication方法 this.unsuccessfulAuthentication(request, response, var9); return; } if(this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //验证成功,调用子类successfulAuthentication方法 this.successfulAuthentication(request, response, chain, authResult); } }
2 JwtAuthenticationTokenFilter
@Component public class JwtAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private ObjectMapper mapper; @Value("${jwt.header}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Value("${jwt.appExpiration}") private Long appExpiration; @Value("${jwt.pcExpiration}") private Long pcExpiration; public JwtAuthenticationTokenFilter(RequestMatcher requiresAuthenticationRequestMatcher) { super(requiresAuthenticationRequestMatcher); setAuthenticationManager(authentication -> authentication); } @Override public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException { //获得header String authHeader = httpServletRequest.getHeader(this.tokenHeader); throwException(authHeader == null, new TokenVerifyException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "令牌缺失")); throwException(!authHeader.startsWith(this.tokenHead), new TokenVerifyException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "令牌格式不正确")); String authToken = authHeader.substring(this.tokenHead.length()); UserDetails userDetails; try { Claims claims = jwtTokenUtil.getClaimsFromToken(authToken); throwException(claims.getSubject() == null, new TokenVerifyException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "令牌格式不正确")); // 校验密码是否已修改 userDetails = this.verifyPasswordChanged(claims.getSubject(), claims.getIssuedAt()); } catch (ExpiredJwtException ex) { this.refreshToken(httpServletResponse, authToken); throw new TokenParseException(RESPONSE_CODE.BACK_CODE_ACCESS_KEY_NOT_EFFECT, "令牌已过期"); } catch (UnsupportedJwtException ex) { throw new TokenParseException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "令牌错误"); } catch (MalformedJwtException ex) { throw new TokenParseException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "令牌格式不正确"); } catch (SignatureException ex) { throw new TokenParseException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "签名错误"); } Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); ((UsernamePasswordAuthenticationToken) authentication).setDetails( new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); } return this.getAuthenticationManager().authenticate(authentication); } private void refreshToken(HttpServletResponse response, String oldToken) { try { Claims claims = jwtTokenUtil.getClaimsAllowedClockSkew(oldToken, Integer.MAX_VALUE); String username = claims.getSubject(); this.verifyPasswordChanged(username, claims.getIssuedAt()); String newToken; if (JwtUser.LOGIN_WAY_APP.equals(claims.getAudience())) { newToken = jwtTokenUtil.refreshToken(oldToken, appExpiration); } else if (JwtUser.LOGIN_WAY_PC.equals(claims.getAudience())) { newToken = jwtTokenUtil.refreshToken(oldToken, pcExpiration); } else { throw new TokenParseException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "未识别的客户端"); } response.addHeader(this.tokenHeader, this.tokenHead + newToken); } catch (ExpiredJwtException ex) { throw new TokenParseException(RESPONSE_CODE.BACK_CODE_RELOGIN, "登录已过期"); } catch (UnsupportedJwtException ex) { throw new TokenParseException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "令牌错误"); } catch (MalformedJwtException ex) { throw new TokenParseException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "令牌格式不正确"); } catch (SignatureException ex) { throw new TokenParseException(RESPONSE_CODE.BACK_CODE_UNAUTHORIZED, "签名错误"); } } //验证密码是否变更 private UserDetails verifyPasswordChanged(String username, Date tokenCreateTime) { JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.isCreatedBeforeLastPasswordReset(tokenCreateTime, user.getLastPasswordResetDate())) { throw new TokenVerifyException(RESPONSE_CODE.BACK_CODE_RELOGIN, "密码变更,需重新登录"); } return user; } private void throwException(boolean expression, AuthenticationException ex) { if (expression) { throw ex; } } //认证结果放入缓存 @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(authResult); chain.doFilter(request, response); } //认证失败,封装返回信息 @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { BackResult<String> result = new BackResult<>(); result.setCode(RESPONSE_CODE.BACK_CODE_EXCEPTION.value); if (failed instanceof TokenVerifyException) { TokenVerifyException ex = (TokenVerifyException) failed; result.setCode(ex.getResponseCode().value); result.setExceptions(ex.getMessage()); } else if (failed instanceof TokenParseException) { TokenParseException ex = (TokenParseException) failed; result.setCode(ex.getResponseCode().value); result.setExceptions(ex.getMessage()); } else { logger.error("Token校验异常", failed); result.setExceptions(SystemConstant.HIDE_ERROR_TIP); } response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); mapper.writeValue(response.getWriter(), result); } }
3 SkipPathRequestMatcher
public class SkipPathRequestMatcher implements RequestMatcher { private OrRequestMatcher matchers; private RequestMatcher processingMatcher; public SkipPathRequestMatcher(@NotEmpty List<String> pathsToSkip, String processingPath) { List<RequestMatcher> m = pathsToSkip.stream().map(AntPathRequestMatcher::new).collect(Collectors.toList()); matchers = new OrRequestMatcher(m); processingMatcher = new AntPathRequestMatcher(processingPath); } @Override public boolean matches(HttpServletRequest request) { return !matchers.matches(request) && processingMatcher.matches(request); } } WebSecurityConfig 类中定义SkipPathRequestMatcher public static final String TOKEN_BASED_ENTRY_POINT = "/limit/**"; public static final String TOKEN_AUTH_ENTRY_POINT = "/auth/**"; public static final String TOKEN_LOGIN_ENTRY_POINT = "/login/**"; public static final String TOKEN_OPEN_ENTRY_POINT = "/open/**"; //路径匹配器,跳过/auth/**类请求,验证/limit/**类请求 @Bean public SkipPathRequestMatcher skipPathRequestMatcher() { List<String> pathsToSkip = Collections.singletonList(TOKEN_AUTH_ENTRY_POINT); return new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_ENTRY_POINT); } //JwtAuthenticationTokenFilter设置路径匹配器 @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() { return new JwtAuthenticationTokenFilter(skipPathRequestMatcher()); }