You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring ControllerAdvice未捕获ExpiredJwtException,返回403而非定义的401

问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 13:38:15