Springboot使用JWT认证过程中BadCredentialsException异常的统一处理方式

184 阅读2分钟

Springboot中一般使用ResponseEntityExceptionHandler统一异常处理,封装到ReplyVO中。

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    public static final String MSG_ACCESS_DENIED_EXCEPTION = "权限受限无法访问";

    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<ReplyVO<String>> accessDeniedException(AccessDeniedException ex) {
        return getResponseEntityReplyVO(MSG_ACCESS_DENIED_EXCEPTION, HttpStatus.PAYMENT_REQUIRED);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ReplyVO<String>> handleIllegalArgumentException(IllegalArgumentException ex) {
        return getResponseEntityReplyVO(ex.getMessage(), HttpStatus.BAD_REQUEST);
    }

    /**
     * 获取响应实体回复VO
     *
     * @param strMessage 消息
     * @param httpStatus http状态
     * @return {@link ResponseEntity }<{@link ReplyVO }<{@link String }>>
     */
    private ResponseEntity<ReplyVO<String>> getResponseEntityReplyVO(String strMessage, HttpStatus httpStatus) {
        return new ResponseEntity<>(new ReplyVO<>(strMessage, ReplyEnum.ERROR_SERVER_ERROR), new HttpHeaders(), httpStatus);
    }
}

但是在处理JWT时,需要配置@EnableWebSecurity的,将JWT的过滤器放到过LogoutFilter前面,这样就造成JWT验证过程中抛出的BadCredentialsException无法在ResponseEntityExceptionHandler中统一处理。

图片.png

为实现统一返回统一格式的JSON结构,需要对BadCredentialsException单独进行处理

    /**
     * JWT请求过滤
     */
    private final JwtRequestFilter jwtRequestFilter;
    
    /**
     * 安全过滤链
     *
     * @param httpSecurity http安全性
     * @return {@link SecurityFilterChain}
     * @throws Exception 异常
     */
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        ......
        // 在过滤器之前加入JWT请求过滤器
        httpSecurity.addFilterBefore(jwtRequestFilter, LogoutFilter.class);
        ......
    }

配置过滤器验证JWT签名的有效性JwtRequestFilter.java,错误的签名抛出BadCredentialsException异常,且catch之后需要调用AuthenticationEntryPointresponse中注入内容。这样我们就可以对BadCredentialsException异常统一进行处理


/**
 * JWT请求过滤
 *
 * @author simen
 * @date 2022/02/10
 */
@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    public static final String MSG_LOGIN_TOKEN_NOT_EXIST = "用户登录信息不存在";
    public static final String MSG_SIGNATURE_FAILED = "用户签名验证失败";
    public static final String MSG_USER_SIGNATURE_EXPIRED = "用户签名已过期";

    final AuthenticationEntryPoint authenticationEntryPoint;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            ......
            // 判断token是否存在
            if (StrUtil.isBlank(strToken)) {
                throw new BadCredentialsException(MSG_LOGIN_TOKEN_NOT_EXIST);
            }

            // 校验签名
            try {
                if (!JWTUtil.verify(strToken, SM2_JWT_SIGNER)) {
                    throw new BadCredentialsException(MSG_SIGNATURE_FAILED);
                }
            } catch (Exception e) {
                throw new BadCredentialsException(MSG_SIGNATURE_FAILED);
            }
            ......
        } catch (AuthenticationException exception) {
            securityContextHolderStrategy.clearContext();
            authenticationEntryPoint.commence(request, response, exception);
            return;
        }
        // 使用过滤链进行过滤
        filterChain.doFilter(request, response);
    }

}

JwtAuthenticationEntryPoint.java对应authenticationEntryPoint.commence(request, response, exception),将所有的异常封装到ReplyVO


/**
 * JWT认证入口点
 *
 * @author Simen
 * @date 2022/02/10
 */
@Slf4j
@Component("JwtAuthenticationEntryPoint")
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // 从request头中获取Accept
        String strAccept = request.getHeader("Accept");
        if (StrUtil.isNotBlank(strAccept)) {
            // 对Accept分组为字符串数组
            String[] strsAccept = StrUtil.splitToArray(strAccept, ",");
            // 判断Accept数组中是否存在"text/html"
            if (ArrayUtil.contains(strsAccept, "text/html")) {
                // 存在"text/html",判断为html访问,则跳转到登录界面
                response.sendRedirect(STR_URL_LOGIN_URL);
            } else {
                // 不存在"text/html",判断为json访问,则返回未授权的json
                SecurityUtils.returnReplyJsonResponse(response, HttpServletResponse.SC_OK,
                        new ReplyVO<>(authException.getMessage(), ReplyEnum.ERROR_SERVER_ERROR));
            }
        }
    }
}