Spring Security 过滤链流程:7 大核心过滤器执行顺序详解
目录
5.1 SecurityContextPersistenceFilter — 上下文管家
5.3 UsernamePasswordAuthenticationFilter — 登录认证
5.4 ExceptionTranslationFilter — 异常守门员 🔑 最关键
5.5 FilterSecurityInterceptor — 最终鉴权
场景 A:注册在 UsernamePasswordAuthenticationFilter 之前
场景 B:注册在 FilterSecurityInterceptor 之前(正确做法)
⚠️ 避坑 1:错误地配在 UsernamePasswordAuthenticationFilter 之前
⚠️ 避坑 2:JWT 过滤器中抛普通 RuntimeException
⚠️ 避坑 3:忘记配置 sessionCreationPolicy
① 导读卡片
| 项目 | 内容 |
|---|---|
| 一句话定位 | 彻底搞懂 Spring Security 5.x 过滤器链的固定执行顺序,以及为什么自定义过滤器的注册位置决定你的代码能不能正常工作 |
| 适合人群 | 已经部分了解 Spring Security,但总是搞不清过滤器的执行顺序和交互关系的后端开发者 |
| 难度 | ⭐⭐⭐⭐(需要知道 Security 基础概念和过滤器基本用法) |
| 阅读时长 | 12 分钟 |
| 前置知识 | Spring Security 基本认识、有过 JWT 整合经验更好 |
② 背景与目标
为什么你需要看懂过滤链?
很多人在 Spring Security 里写完自定义过滤器后,发现:
-
Token 过期了居然返回 500,而不是 401
-
明明写了
AuthenticationEntryPoint,但它就是不被调用 -
认证逻辑走了一遍又一遍,搞不清到底是谁处理的
这些问题的根源就一个:不知道过滤器链的执行顺序。
学完你能干什么
-
能画出一个标准请求经过的所有 Security 过滤器顺序
-
知道自定义过滤器放在哪个位置,异常才能被正确处理
-
从此不再「猜」配置的效果,而是「推」出配置的结果
③ 概念与原理
3.1 什么是过滤器链?
Spring Security 本质上是一个过滤器链(Filter Chain)。每个请求进来后,像流水线上的产品一样,逐个经过每个过滤器。只要有一个过滤器决定「这个请求不合法」,请求就到此为止。
请求 → Filter1 → Filter2 → Filter3 → ... → Controller
3.2 过滤器链为什么有严格顺序?
每个过滤器的职责不同,它们之间存在依赖关系:
-
有些过滤器负责把上下文准备好(
SecurityContextPersistenceFilter) -
有些负责认证(
UsernamePasswordAuthenticationFilter) -
有些负责异常处理(
ExceptionTranslationFilter) -
有些负责最终鉴权(
FilterSecurityInterceptor)
如果顺序乱了,后面的过滤器依赖的数据还没准备好,整个认证体系就会瘫痪。
3.3 顺序谁定的?能改吗?
Spring Security 5.x 的内置过滤器顺序定义在 FilterComparator 类中。内置过滤器的顺序是固定的,你不能改。但你可以在内置过滤器之间「插入」你的自定义过滤器,通过 addFilterBefore 或 addFilterAfter 来定位。
④ 逻辑与对比:七大核心过滤器的职责分工
下面这张表,是 Spring Security 5.x 中最核心的 7 个过滤器,从左到右按执行顺序排列:
各自管什么?
| 过滤器 | 职责 | 重要程度 |
|---|---|---|
SecurityContextPersistenceFilter |
请求开始前恢复 SecurityContext,请求结束后清理 |
⭐⭐ |
LogoutFilter |
处理登出请求 | ⭐⭐ |
UsernamePasswordAuthenticationFilter |
处理表单登录,验证账号密码 | ⭐⭐⭐ |
ExceptionTranslationFilter |
异常翻译器 — 捕获后面过滤器的认证/授权异常,发给对应的 Handler | ⭐⭐⭐⭐⭐ |
FilterSecurityInterceptor |
最终鉴权 — 从上下文拿 Authentication 做权限校验 |
⭐⭐⭐⭐⭐ |
| 你的 JWT 过滤器(自定义) | 解析 Token,转换成 Security 能识别的认证对象 | ⭐⭐⭐⭐ |
⑤ 核心详解
5.1 SecurityContextPersistenceFilter — 上下文管家
位置:过滤器链第一个
职责:
-
请求开始时:从 Session(或 SecurityContextRepository)中恢复
SecurityContext -
请求结束后:把
SecurityContext保存回去,然后清空
为什么需要它:SecurityContextHolder.getContext() 在每个请求中都能拿到正确的认证信息,靠的就是它。
5.2 LogoutFilter — 登出处理
位置:第二个
职责:拦截登出请求(默认 /logout),清除认证信息、使 Session 失效。
5.3 UsernamePasswordAuthenticationFilter — 登录认证
位置:第三个
职责:拦截登录请求(默认 /login 的 POST),从请求中取 username 和 password,封装成 UsernamePasswordAuthenticationToken,交给 AuthenticationManager 认证。
注意:如果你像主流做法一样把 /login 设置成了 permitAll 并且在 Controller 里手动认证,这个过滤器就不会被触发。
5.4 ExceptionTranslationFilter — 异常守门员 🔑 最关键
位置:在所有认证过滤器之后,在 FilterSecurityInterceptor 之前
职责:
-
不处理正常请求,直接放行
-
只在后面的过滤器抛出异常时才出手
它的核心机制:
// ExceptionTranslationFilter 的伪代码逻辑
try {
filterChain.doFilter(request, response); // 放行给后面的过滤器
} catch (AuthenticationException e) {
// 认证异常 → 调用 AuthenticationEntryPoint
authenticationEntryPoint.commence(request, response, e);
} catch (AccessDeniedException e) {
// 授权异常 → 调用 AccessDeniedHandler
accessDeniedHandler.handle(request, response, e);
}
最重要的一句话:它只捕获在它之后执行的过滤器抛出的异常。
5.5 FilterSecurityInterceptor — 最终鉴权
位置:过滤器链最后一个
职责:
-
从
SecurityContextHolder.getContext()获取Authentication对象 -
检查当前请求的 URL 或方法需要的权限
-
如果用户没有对应权限 → 抛出
AccessDeniedException -
如果用户未认证 → 抛出
InsufficientAuthenticationException(这是AuthenticationException的子类)
这就是为什么:你的自定义过滤器必须在它之前运行,把认证对象塞进上下文,否则它拿不到用户信息,直接抛异常。
5.6 自定义过滤器的位置选择(核心中的核心)
场景 A:注册在 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
执行顺序:
SecurityContextPersistenceFilter→LogoutFilter→jwtFilter→UsernamePasswordAuthenticationFilteptionTranslationFilter → FilterSecurityInterceptor
结果:jwtFilter 在 ExceptionTranslationFilter 前面执行。 如果你的 jwtFilter 抛 AuthenticationException,它会越过 ExceptionTranslationFilter,直接抛到 Tomcat 容器,返回 500 错误。 你配置的 AuthenticationEntryPoint 根本不会生效!
场景 B:注册在 FilterSecurityInterceptor 之前(正确做法)
http.addFilterBefore(jwtFilter, FilterSecurityInterceptor.class);
执行顺序:
... → ExceptionTranslationFilter → jwtFilter → FilterSecurityInterceptor
结果:jwtFilter 在 ExceptionTranslationFilter 后面执行。 当 jwtFilter 抛出 AuthenticationException 时,异常反向传递到 ExceptionTranslationFilter,被它捕获,然后调用 AuthenticationEntryPoint,返回你定义好的 401 JSON 响应。
这就是你项目中实际看到的正确行为!
⑥ 案例实战
场景:配置一个完整的、可正常工作的过滤器链
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private AuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private AccessDeniedHandler jwtAccessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
// ⭐ 正确位置:JWT 过滤器放在 ExceptionTranslationFilter 之后
// 这样异常才能被统一捕获
.addFilterBefore(jwtAuthenticationFilter, FilterSecurityInterceptor.class)
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint) // 401
.accessDeniedHandler(jwtAccessDeniedHandler) // 403
.and()
// 禁用默认表单登录
.formLogin().disable()
// 禁用 httpBasic
.httpBasic().disable();
}
}
请求流转的完整过程
【请求到达】
↓
① SecurityContextPersistenceFilter
→ 从 Session/Context 恢复 SecurityContext
↓
② LogoutFilter
→ 检查是否是登出请求?不是 → 放行
↓
③ UsernamePasswordAuthenticationFilter
→ 检查是否有 username/password?没有 → 放行
↓
④ ... 其他内置过滤器
↓
⑤ ExceptionTranslationFilter
→ try { 放行给后面的过滤器 }
↓
⑥ JwtAuthenticationFilter(你的)
→ 取 Token → 解析 → 封装 Authentication → 塞入 SecurityContext → 继续
↓
⑦ FilterSecurityInterceptor
→ 从 SecurityContext 取 Authentication
→ 检查当前 URL 需要的权限
→ 有权限 → 放行到 Controller
→ 没权限 → throw AccessDeniedException(被 ⑤ 捕获)
↓
【Controller 执行业务逻辑】
⑦ 避坑 & 最佳实践
⚠️ 避坑 1:错误地配在 UsernamePasswordAuthenticationFilter 之前
这是最常见的错误。很多人跟着教程写:
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
结果 JWT 过滤器的异常永远触达不到 AuthenticationEntryPoint,永远返回 500。
记住:UsernamePasswordAuthenticationFilter 在 ExceptionTranslationFilter 之前,所以配在它前面意味着你也在异常拦截器前面。
⚠️ 避坑 2:JWT 过滤器中抛普通 RuntimeException
如果 JWT 过滤器里抛的是 ExpiredJwtException 或者 JwtException,它们是 RuntimeException,不是 AuthenticationException,ExceptionTranslationFilter不会捕获它们。
解决方案:在 JWT 过滤器中 catch 后转换成 BadCredentialsException(它是 AuthenticationException 的子类)再抛出。
⚠️ 避坑 3:忘记配置 sessionCreationPolicy
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
如果不配,Security 默认会创建 Session,可能导致 JWT 的无状态认证和 Session 冲突。
✅ 最佳实践清单
-
自定义认证过滤器用
addFilterBefore(yourFilter, FilterSecurityInterceptor.class)这是让异常被 Security 统一处理的关键 -
JWT 过滤器中抛出
BadCredentialsException而不是原始的ExpiredJwtException -
开启 DEBUG 日志验证过滤器链:
logging.level.org.springframework.security=DEBUG -
一定要配
AuthenticationEntryPoint+AccessDeniedHandler统一异常响应格式
⑧ 总结 & 路线图
一句话记住
过滤器顺序决定异常归宿:自定义过滤器放在 ExceptionTranslationFilter 后面,抛出的认证异常才能被统一捕获;放在前面,异常直接冲到 Tomcat 返回 500。
过滤器链记忆口诀
上下文中恢复 → 登出处理 → 表单登录 → 中间其他 → 异常守门 → 你的认证 → 最后鉴权 (SecurityContextPersistence → Logout → UPF → ... → ET → YourFilter → FSI)
下一步推荐阅读
-
✅ Spring Security 过滤链异常捕获 — 异常在过滤器链中怎么传递、去哪了
-
✅ Spring Security 异常捕获机制 — Security 异常处理器与全局异常处理器的分工
-
✅ Spring Security 的两类异常 — AuthenticationException 和 AccessDeniedException 完全分清
更多推荐





所有评论(0)