Spring ControllerAdvice未捕获ExpiredJwtException,返回403而非定义的401
问题详情
我在Spring的@ControllerAdvice中定义了异常处理逻辑,期望Token过期时返回401(Unauthorized)状态码及自定义ErrorObject:
@ExceptionHandler(DataIntegrityViolationException.class) public ResponseEntity handleRegisterUsernameDuplication(DataIntegrityViolationException e){ ErrorObject errorObject = new ErrorObject( HttpStatus.CONFLICT.value(), e.getMessage(), new Date() ); return new ResponseEntity<>(errorObject, HttpStatus.CONFLICT); } @ExceptionHandler(ExpiredJwtException.class) public ResponseEntity<ErrorObject> handleJwtExpiration(ExpiredJwtException e){ ErrorObject errorObject = new ErrorObject( HttpStatus.UNAUTHORIZED.value(), e.getMessage(), new Date() ); return new ResponseEntity<>(errorObject, HttpStatus.UNAUTHORIZED); }
但实际运行时:
- 服务器端明确抛出
ExpiredJwtException:
2023-12-06T17:01:13.459+01:00 ERROR 11489 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception io.jsonwebtoken.ExpiredJwtException: JWT expired at 2023-12-06T16:01:12Z. Current time: 2023-12-06T16:01:13Z, a difference of 1457 milliseconds. Allowed clock skew: 0 milliseconds. at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:427) ~[jjwt-impl-0.11.5.jar:0.11.5]
- 客户端却收到403(Forbidden)错误:
ERROR Request failed with status code 403 AxiosError@http://localhost:3000/static/js/bundle.js:66657:18 settle@http://localhost:3000/static/js/bundle.js:67310:12 onloadend@http://localhost:3000/static/js/bundle.js:65992:6
原因分析
1. 异常抛出时机超出@ControllerAdvice的拦截范围
@ControllerAdvice仅能捕获控制器(Controller)层级抛出的异常。而JWT验证通常在**过滤器(Filter)或拦截器(Interceptor)**阶段执行(属于请求进入Controller之前的前置处理),此时抛出的ExpiredJwtException不会被@ControllerAdvice捕获,而是由Spring Security的默认异常处理机制处理,返回403状态码。
2. Spring Security的异常处理优先级更高
即使异常在Controller层级抛出,Spring Security的ExceptionTranslationFilter会优先处理认证/授权类异常,覆盖@ControllerAdvice的处理逻辑,返回默认的403响应。
解决方案
方案1:自定义Spring Security认证失败处理器
实现AuthenticationEntryPoint接口,专门处理Token过期等认证异常:
@Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { // 判断异常根源是否为JWT过期 if (authException.getCause() instanceof ExpiredJwtException) { ErrorObject errorObject = new ErrorObject( HttpStatus.UNAUTHORIZED.value(), authException.getMessage(), new Date() ); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(errorObject)); } else { // 处理其他认证异常 response.sendError(HttpStatus.UNAUTHORIZED.value(), authException.getMessage()); } } }
在Spring Security配置中指定该处理器:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomAuthenticationEntryPoint authenticationEntryPoint; @Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) // 绑定自定义处理器 .and() // 其他Security配置(如JWT过滤器、权限规则等) .authorizeRequests() .antMatchers("/auth/**").permitAll() .anyRequest().authenticated(); } }
方案2:在自定义JWT过滤器中直接捕获异常
如果你的JWT验证是通过自定义过滤器实现的,可在过滤器内部捕获ExpiredJwtException,直接构建响应返回:
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private ObjectMapper objectMapper; private final String secretKey = "your-secret-key"; // 替换为实际密钥 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { // 提取并验证Token String token = extractTokenFromRequest(request); if (token != null) { Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); } filterChain.doFilter(request, response); } catch (ExpiredJwtException e) { // 构建自定义错误响应 ErrorObject errorObject = new ErrorObject( HttpStatus.UNAUTHORIZED.value(), e.getMessage(), new Date() ); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(errorObject)); } } private String extractTokenFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }
方案3:将JWT验证移至Controller/Service层(不推荐)
若业务场景允许,可将JWT验证逻辑移到Controller或Service层执行,这样@ControllerAdvice就能捕获到ExpiredJwtException。但此方式不符合认证逻辑前置处理的规范,可能导致未授权请求进入业务逻辑,不建议使用。
内容的提问来源于stack exchange,提问作者milanHrabos

