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());
    }

 

posted @ 2018-10-24 18:49  allenli263  阅读(842)  评论(0编辑  收藏  举报