目录
[① 导读卡片](#① 导读卡片)
[② 背景与目标](#② 背景与目标)
[③ 概念与原理](#③ 概念与原理)
[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)
下一步推荐阅读
-
✅ Spring Security 异常捕获机制 --- Security 的
exceptionHandling()两个配置怎么分工 -
✅ Spring Security 的两类异常 --- 分清 AuthenticationException 和 AccessDeniedException
-
✅ JwtAuthenticationFilter 详解 --- JWT 过滤器核心条件判断逐行拆解