Spring Security 过滤链异常捕获:为什么 JWT 过滤器的异常全局 @RestControllerAdvice 抓不到?

目录

[① 导读卡片](#① 导读卡片)

[② 背景与目标](#② 背景与目标)

为什么你会遇到这个问题?

学完你能干什么

[③ 概念与原理](#③ 概念与原理)

[3.1 请求的完整执行阶段](#3.1 请求的完整执行阶段)

[3.2 全局异常处理器的作用范围](#3.2 全局异常处理器的作用范围)

[3.3 异常的实际去向](#3.3 异常的实际去向)

[④ 逻辑与对比:处理 Filter 异常的三种方案](#④ 逻辑与对比:处理 Filter 异常的三种方案)

[⑤ 核心详解](#⑤ 核心详解)

[5.1 方案一:Filter 内部 try-catch](#5.1 方案一:Filter 内部 try-catch)

[5.2 方案二:AuthenticationEntryPoint(官方推荐 ⭐)](#5.2 方案二:AuthenticationEntryPoint(官方推荐 ⭐))

[第 1 步:创建异常处理器](#第 1 步:创建异常处理器)

[第 2 步:在 SecurityConfig 中配置](#第 2 步:在 SecurityConfig 中配置)

[第 3 步:JWT 过滤器直接抛异常,无需 try-catch](#第 3 步:JWT 过滤器直接抛异常,无需 try-catch)

[5.3 方案三:AuthenticationEntryPoint + AccessDeniedHandler 组合](#5.3 方案三:AuthenticationEntryPoint + AccessDeniedHandler 组合)

[⑥ 案例实战](#⑥ 案例实战)

场景:三种异常在项目中的实际处理路径

[⑦ 避坑 & 最佳实践](#⑦ 避坑 & 最佳实践)

[⚠️ 避坑 1:JWT 过滤器放在 ExceptionTranslationFilter 前面](#⚠️ 避坑 1:JWT 过滤器放在 ExceptionTranslationFilter 前面)

[⚠️ 避坑 2:忘记在 Filter 中 return](#⚠️ 避坑 2:忘记在 Filter 中 return)

[⚠️ 避坑 3:直接用 try-catch 后忘记设置状态码](#⚠️ 避坑 3:直接用 try-catch 后忘记设置状态码)

[✅ 最佳实践选择指南](#✅ 最佳实践选择指南)

[⑧ 总结 & 路线图](#⑧ 总结 & 路线图)

一句话记住

两张图秒懂

下一步推荐阅读



① 导读卡片

项目 内容
一句话定位 搞懂为什么 JWT 过滤器抛的异常走不到 Spring 的全局异常拦截器,以及如何用 Spring Security 内置机制优雅地统一处理
适合人群 已经整合了 Security + JWT,但发现过滤器异常无法被 @RestControllerAdvice 捕获的开发者
难度 ⭐⭐⭐(需要了解 Security 基本概念和请求执行顺序)
阅读时长 8 分钟
前置知识 Spring Security 过滤器链基本概念、JWT 过滤器的基本写法

② 背景与目标

为什么你会遇到这个问题?

你在项目中写了 @RestControllerAdvice 全局异常处理器,捕获了 Controller 层的所有异常。

但当 JWT 过滤器里 Token 过期、签名错误时,你发现:

复制代码
❌ 全局异常处理器根本没有执行!
❌ 返回的是 Tomcat 默认的错误页面!

你开始怀疑:难道过滤器抛的异常不走拦截器吗?

答案是:确实不走。这不是你代码写错了,而是 Filter(过滤器)的执行阶段根本不在 Spring MVC 范围内。

学完你能干什么

  • 准确说出为什么 Filter 异常进不了 @RestControllerAdvice

  • 掌握两种方案处理 Filter 异常:try-catch 和 Security 的 AuthenticationEntryPoint

  • 能根据项目需要选择合适的异常处理方式


③ 概念与原理

3.1 请求的完整执行阶段

一个请求到达后端后,会经过三个不同的层级:

复制代码
【Tomcat 容器】
    ↓
【Spring Security 过滤器链】 ⭐ 这里包含你的 JWT Filter
    ↓
【Spring MVC 拦截器 Interceptor】
    ↓
【Controller + @RestControllerAdvice】

关键结论 :过滤器(Filter)属于 Tomcat 容器阶段,而 @RestControllerAdvice 属于 Spring MVC 阶段。过滤器抛异常时,请求还没进入 Spring MVC 容器。

3.2 全局异常处理器的作用范围

@RestControllerAdvice 只处理:

  • Controller 方法中抛出的异常

  • 拦截器(Interceptor)中抛出的异常

不处理

  • Filter(过滤器)中抛出的异常

  • Security 过滤器链中抛出的异常

3.3 异常的实际去向

当 JWT 过滤器抛出异常时:

复制代码
JWT Filter 抛异常
    ↓
❌ 不走 Spring MVC 拦截器
❌ 不走 @RestControllerAdvice
    ↓
异常直接抛回 Tomcat 容器
    ↓
Tomcat 返回默认错误页面(500 / 白页 / 纯文本错误)

④ 逻辑与对比:处理 Filter 异常的三种方案

方案 描述 推荐度
方案一:Filter 内 try-catch 在 JWT 过滤器里自己 try-catch,手动返回 JSON ⭐⭐⭐ 简单直接
方案二:Security AuthenticationEntryPoint 在 SecurityConfig 中配置异常处理器,统一捕获 ⭐⭐⭐⭐⭐ 官方推荐
方案三:全局 @RestControllerAdvice ❌ 做不到!不适用于 Filter 阶段 ⭐ 不可行

⑤ 核心详解

5.1 方案一:Filter 内部 try-catch

这是最直接的方法。在你的 JwtAuthenticationFilter 中,自己处理异常:

java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
​
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
​
        String token = extractToken(request);
​
        if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            try {
                // 解析 Token(可能抛异常)
                Claims claims = jwtUtil.parseJwt(token);
​
                // 封装认证对象
                List<String> authorities = claims.get("perms", List.class);
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        claims.get("username"), null,
                        AuthorityUtils.commaSeparatedStringToAuthorityList(
                            String.join(",", authorities)
                        )
                    );
                SecurityContextHolder.getContext().setAuthentication(authToken);
​
            } catch (Exception e) {
                // 👇 在这里手动处理异常,返回 JSON
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write(
                    JSON.toJSONString(R.fail(401, "Token无效或已过期"))
                );
                return; // ⭐ 必须 return,不再执行后续过滤器链
            }
        }
​
        filterChain.doFilter(request, response);
    }
​
    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

优点 :简单直接,所有逻辑在一个类里完成。 缺点:异常处理逻辑耦合在过滤器中,如果多个过滤器都有类似的逻辑,会重复。

5.2 方案二:AuthenticationEntryPoint(官方推荐 ⭐)

第 1 步:创建异常处理器
java 复制代码
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
​
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
​
        // 统一返回 R 格式的 JSON
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(
            JSON.toJSONString(R.fail(401, "Token无效或已过期,请重新登录"))
        );
    }
}
第 2 步:在 SecurityConfig 中配置
java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
​
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
            .and()
            // ⭐ 关键:JWT 过滤器放在 FilterSecurityInterceptor 之前
            // 这样异常会被前面的 ExceptionTranslationFilter 捕获
            .addFilterBefore(jwtAuthenticationFilter, FilterSecurityInterceptor.class)
            .exceptionHandling()
                // 认证异常(Token 无效/过期) → 401
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
第 3 步:JWT 过滤器直接抛异常,无需 try-catch
java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
​
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
​
        String token = extractToken(request);
​
        if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // ⭐ 直接解析,失败抛异常,不用 try-catch!
            Claims claims = jwtUtil.parseJwt(token);
            // ... 后续逻辑
            UsernamePasswordAuthenticationToken authToken = buildAuthToken(claims);
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
​
        filterChain.doFilter(request, response);
    }
}

关键原理 :JWT 过滤器抛出异常后,因为过滤器位置在 ExceptionTranslationFilter后面 ,异常会反向传递,被 ExceptionTranslationFilter 捕获,然后调用你配置的 AuthenticationEntryPoint

5.3 方案三:AuthenticationEntryPoint + AccessDeniedHandler 组合

如果要同时处理认证异常和授权异常,可以配置两个 Handler:

java 复制代码
.exceptionHandling()
    // 认证失败(未登录/Token无效) → 401
    .authenticationEntryPoint(jwtAuthenticationEntryPoint)
    .and()
.exceptionHandling()
    // 授权失败(已登录但没权限) → 403
    .accessDeniedHandler(jwtAccessDeniedHandler)

⑥ 案例实战

场景:三种异常在项目中的实际处理路径

java 复制代码
// ========== 异常 1:登录时账号密码错误 ==========
// 发生位置:LoginController 中 authenticationManager.authenticate(...)
// 捕获者:@RestControllerAdvice(全局异常处理器)
// 原因:异常在 Controller 层抛出
​
// ========== 异常 2:JWT Token 过期 ==========
// 发生位置:JwtAuthenticationFilter 中 jwtUtil.parseJwt(token)
// 捕获者:AuthenticationEntryPoint(Security 异常处理器)
// 原因:异常在 Security 过滤器链中抛出
​
// ========== 异常 3:已登录但访问了没有权限的接口 ==========
// 发生位置:FilterSecurityInterceptor 中权限校验
// 捕获者:AccessDeniedHandler(Security 异常处理器)
// 原因:异常在 Security 过滤器链中抛出

完整异常处理配置:

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
​
    @Autowired
    private AuthenticationEntryPoint unauthorizedHandler;
​
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/admin/**").hasRole("admin")
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter, FilterSecurityInterceptor.class)
            .exceptionHandling()
                .authenticationEntryPoint(unauthorizedHandler)   // 401
                .accessDeniedHandler(accessDeniedHandler)         // 403
            .and()
            .formLogin().disable()
            .httpBasic().disable();
    }
}

⑦ 避坑 & 最佳实践

⚠️ 避坑 1:JWT 过滤器放在 ExceptionTranslationFilter 前面

java 复制代码
// ❌ 错误写法:异常不会被 Security 捕获
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
​
// ✅ 正确写法:异常被 ExceptionTranslationFilter 捕获
.addFilterBefore(jwtFilter, FilterSecurityInterceptor.class)

⚠️ 避坑 2:忘记在 Filter 中 return

JWT 过滤器 catch 异常后手动写了响应,但没有 return

java 复制代码
catch (Exception e) {
    response.getWriter().write(json);  // 写了响应
    // ❌ 忘记 return,继续执行 doFilter
}
filterChain.doFilter(request, response); // 继续执行!出现重复写入错误

一定要在最后加 return

java 复制代码
catch (Exception e) {
    response.getWriter().write(json);
    return; // ✅ 阻止继续执行过滤器链
}

⚠️ 避坑 3:直接用 try-catch 后忘记设置状态码

java 复制代码
catch (Exception e) {
    response.setContentType("application/json;charset=utf-8");
    // ❌ 忘记设置状态码,默认 200
    response.getWriter().write(JSON.toJSONString(R.fail(401, "无效")));
}

正确做法

复制代码
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 设置 401

✅ 最佳实践选择指南

你的情况 推荐方案
项目简单,过滤器少 方案一(Filter 内 try-catch)
企业级项目,需要统一异常格式 方案二(AuthenticationEntryPoint)
已有 RuoYi 等框架项目 用框架自带的方式(通常是方案二)

⑧ 总结 & 路线图

一句话记住

Filter 异常 → Tomcat 处理 | Controller 异常 → @RestControllerAdvice 处理,两者不在同一个层级,Filter 异常走不到全局异常处理器。

复制代码
Filter 异常路径:
JWT Filter 抛异常 → ExceptionTranslationFilter(Security) → AuthenticationEntryPoint
​
Controller 异常路径:
Controller 抛异常 → @RestControllerAdvice(Spring MVC)

下一步推荐阅读