目录

① 导读卡片

② 背景与目标

为什么你需要看懂过滤链?

学完你能干什么

③ 概念与原理

3.1 什么是过滤器链?

3.2 过滤器链为什么有严格顺序?

3.3 顺序谁定的?能改吗?

④ 逻辑与对比:七大核心过滤器的职责分工

各自管什么?

⑤ 核心详解

5.1 SecurityContextPersistenceFilter — 上下文管家

5.2 LogoutFilter — 登出处理

5.3 UsernamePasswordAuthenticationFilter — 登录认证

5.4 ExceptionTranslationFilter — 异常守门员 🔑 最关键

5.5 FilterSecurityInterceptor — 最终鉴权

5.6 自定义过滤器的位置选择(核心中的核心)

场景 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 类中。内置过滤器的顺序是固定的,你不能改。但你可以在内置过滤器之间「插入」你的自定义过滤器,通过 addFilterBeforeaddFilterAfter 来定位。


④ 逻辑与对比:七大核心过滤器的职责分工

下面这张表,是 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),从请求中取 usernamepassword,封装成 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 — 最终鉴权

位置:过滤器链最后一个

职责

  1. SecurityContextHolder.getContext() 获取 Authentication 对象

  2. 检查当前请求的 URL 或方法需要的权限

  3. 如果用户没有对应权限 → 抛出 AccessDeniedException

  4. 如果用户未认证 → 抛出 InsufficientAuthenticationException(这是 AuthenticationException 的子类)

这就是为什么:你的自定义过滤器必须在它之前运行,把认证对象塞进上下文,否则它拿不到用户信息,直接抛异常。

5.6 自定义过滤器的位置选择(核心中的核心)

场景 A:注册在 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

执行顺序:

SecurityContextPersistenceFilter→LogoutFilter→jwtFilter→UsernamePasswordAuthenticationFilteptionTranslationFilter → FilterSecurityInterceptor

结果jwtFilterExceptionTranslationFilter 前面执行。 如果你的 jwtFilter 抛 AuthenticationException,它会越过 ExceptionTranslationFilter,直接抛到 Tomcat 容器,返回 500 错误。 你配置的 AuthenticationEntryPoint 根本不会生效!

场景 B:注册在 FilterSecurityInterceptor 之前(正确做法)
http.addFilterBefore(jwtFilter, FilterSecurityInterceptor.class);

执行顺序:

... → ExceptionTranslationFilter → jwtFilter → FilterSecurityInterceptor

结果jwtFilterExceptionTranslationFilter 后面执行。 当 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。

记住UsernamePasswordAuthenticationFilterExceptionTranslationFilter 之前,所以配在它前面意味着你也在异常拦截器前面。

⚠️ 避坑 2:JWT 过滤器中抛普通 RuntimeException

如果 JWT 过滤器里抛的是 ExpiredJwtException 或者 JwtException,它们是 RuntimeException,不是 AuthenticationExceptionExceptionTranslationFilter不会捕获它们

解决方案:在 JWT 过滤器中 catch 后转换成 BadCredentialsException(它是 AuthenticationException 的子类)再抛出。

⚠️ 避坑 3:忘记配置 sessionCreationPolicy

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

如果不配,Security 默认会创建 Session,可能导致 JWT 的无状态认证和 Session 冲突。

✅ 最佳实践清单

  1. 自定义认证过滤器用 addFilterBefore(yourFilter, FilterSecurityInterceptor.class) 这是让异常被 Security 统一处理的关键

  2. JWT 过滤器中抛出 BadCredentialsException 而不是原始的 ExpiredJwtException

  3. 开启 DEBUG 日志验证过滤器链logging.level.org.springframework.security=DEBUG

  4. 一定要配 AuthenticationEntryPoint + AccessDeniedHandler 统一异常响应格式


⑧ 总结 & 路线图

一句话记住

过滤器顺序决定异常归宿:自定义过滤器放在 ExceptionTranslationFilter 后面,抛出的认证异常才能被统一捕获;放在前面,异常直接冲到 Tomcat 返回 500。

过滤器链记忆口诀

上下文中恢复 → 登出处理 → 表单登录 → 中间其他 → 异常守门 → 你的认证 → 最后鉴权
(SecurityContextPersistence → Logout → UPF → ... → ET → YourFilter → FSI)

下一步推荐阅读

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐