Spring Security 过滤器链的工作流程是什么?
简化版
Spring Security 的核心是一条 Servlet Filter 链——它通过一个注册到容器的 DelegatingFilterProxy 把请求引到 Spring 内部的 FilterChainProxy,后者按请求匹配到某条 SecurityFilterChain,再依次执行一串安全过滤器:准备 SecurityContext → 认证过滤器(提取凭证、认证)→ 异常处理过滤器 → 授权过滤器(决定放不放行)。过滤器的顺序非常关键——上下文要先准备好认证才能写入、异常处理要包住后续、授权要在认证之后。一个应用可有多条 SecurityFilterChain(用 matcher 区分 API/管理端/静态资源)。
详细版
整体接入方式:
请求 → 容器 Servlet Filter 链
→ DelegatingFilterProxy(桥接容器 Filter 和 Spring Bean)
→ FilterChainProxy(Spring Security 总入口)
→ 按 matcher 选中一条 SecurityFilterChain
→ 依次执行链上的安全过滤器
一条 SecurityFilterChain 上过滤器的大致顺序(简化):
| 顺序 | 过滤器 | 职责 |
|---|---|---|
| 1 | SecurityContextPersistenceFilter(新版 SecurityContextHolderFilter) | 加载/准备 SecurityContext |
| 2 | 各种认证过滤器(UsernamePasswordAuthenticationFilter、BearerTokenAuthenticationFilter 等) | 提取凭证、发起认证 |
| 3 | ExceptionTranslationFilter | 捕获认证/授权异常,转成 401/403 或跳登录 |
| 4 | AuthorizationFilter(新版;旧版 FilterSecurityInterceptor) | 授权决策,链的末端 |
多条链(用 matcher 区分):
@Bean @Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**") // 只匹配 /api/**
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())); // API 用 JWT
return http.build();
}
@Bean @Order(2)
SecurityFilterChain webChain(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults()); // 页面用表单登录
return http.build();
}
完整版教学
一、入口为什么是 Filter
Spring Security 要在请求到达 Controller 之前完成身份识别和访问控制——如果放到 Controller 或 Interceptor 里就太晚了(有些请求可能绕过 MVC)。Filter 是请求进入应用的最外层(见「Filter vs Interceptor」专题),所以 Security 选择接入 Servlet Filter 体系。
具体机制:Spring Security 往容器注册一个 DelegatingFilterProxy(容器认识的标准 Filter),它把请求委托给 Spring 容器里的 FilterChainProxy Bean。FilterChainProxy 才是真正承载安全逻辑的核心——容器看到的是一个代理入口,内部维护着多条安全过滤器链。这样安全逻辑能享受 Spring 的依赖注入,又能工作在 Filter 层。
二、一条请求如何选择链
一个应用可以声明多条 SecurityFilterChain,分别用于不同类型的请求。请求到来后,FilterChainProxy 按每条链的 matcher(securityMatcher)顺序判断,选中第一条匹配的链,只走这一条。
典型分链:
/api/**→ 用 JWT / Bearer Token 认证(无状态)。/actuator/**→ 单独的监控端点安全策略。- 普通页面 → 表单登录 + Session。
关键:更具体的规则要放前面(用 @Order 控制顺序)——因为匹配到第一条就停,如果把宽泛的链放前面,具体的链永远匹配不到。
| 链顺序 | matcher | 认证方式 | 说明 |
|---|---|---|---|
@Order(1) | /api/** | Bearer JWT | 具体规则先匹配 |
@Order(2) | /actuator/** | 管理端点鉴权 | 单独收紧 |
@Order(3) | /** | Form Login/Session | 兜底页面链 |
多条 SecurityFilterChain 的关键是“第一条匹配即生效”,具体链要排在宽泛链前面。
三、过滤器顺序为什么关键
在一条链内,过滤器的顺序有严格要求,错了就出 bug:
- 准备 SecurityContext 的过滤器要最先——它负责加载/创建
SecurityContext(认证信息的容器),后面的认证过滤器才有地方写入认证结果。 - 认证过滤器要在授权过滤器之前——必须先知道「你是谁」,才能判断「你能不能访问」。
- 异常处理过滤器(
ExceptionTranslationFilter) 要包住后续流程——它要能捕获后面认证过滤器和授权过滤器抛出的异常,转成合适的响应(401/403/跳登录)。 - 授权过滤器(
AuthorizationFilter)在链的末端——此时认证信息已具备,才能做访问决策。
顺序错了,比如授权过滤器跑在认证之前,就会拿不到身份、误判为匿名用户。
四、认证过滤器做什么
不同的登录方式有对应的认证过滤器:表单登录(UsernamePasswordAuthenticationFilter)、HTTP Basic、Bearer Token(BearerTokenAuthenticationFilter)、OAuth2 Login 等。它们的统一流程:
- 从请求里提取凭证(表单的用户名密码、请求头的 Token)。
- 构造一个「未认证的
Authentication」 对象。 - 交给
AuthenticationManager(内部委托给合适的AuthenticationProvider)校验。 - 成功 → 把「已认证的
Authentication」存入SecurityContext;失败 → 触发失败处理(AuthenticationFailureHandler/AuthenticationEntryPoint)。
认证成功后,SecurityContext 里就有了当前用户,后续的授权和业务代码(SecurityContextHolder.getContext().getAuthentication())才能拿到身份。
SecurityContextHolderFilter
-> 认证过滤器
-> ExceptionTranslationFilter
-> AuthorizationFilter
-> Controller
五、异常处理的位置(401 vs 403)
ExceptionTranslationFilter 负责把安全异常转成响应:
- 认证失败/未认证 → 交给
AuthenticationEntryPoint,通常返回 401 或跳转登录页。 - 已认证但权限不足 → 交给
AccessDeniedHandler,通常返回 403。
REST API 要特别注意:默认配置在未认证时可能跳转到登录页面(返回 HTML),这对 API 是错误的——API 应该返回 401 JSON。所以 API 链要自定义 AuthenticationEntryPoint 返回 401,不要被默认的登录页响应污染。
六、permitAll 和「忽略」的区别(易错)
两种「放行」方式有本质区别:
permitAll():请求仍然经过整条安全过滤器链,只是在授权阶段放行。它仍然会准备 SecurityContext、加安全响应头、走异常处理。- 完全忽略某路径(
WebSecurity.ignoring()):请求根本不进安全链,跳过所有安全过滤器——没有 SecurityContext、没有安全响应头、没有审计。
建议:静态资源可以用 ignoring(不需要安全处理);业务接口即使不需要认证,也应该用 permitAll 而非 ignoring——这样它仍受安全链保护(有安全响应头、能被后续安全逻辑感知),更安全。
七、排查过滤器链问题
Spring Security 问题(JWT 不生效、认证丢失、意外 403)排查:
- 开启 debug 日志(
logging.level.org.springframework.security=DEBUG)——能看到请求匹配了哪条链、经过了哪些过滤器、顺序如何。 - 确认请求匹配了哪条 SecurityFilterChain——多链时很可能匹配错了链(matcher 或 @Order 问题)。
- 看自定义过滤器的插入位置——用
addFilterBefore/addFilterAfter插入 JWT 过滤器时,位置错了(如放在授权过滤器之后)会导致认证信息还没写入就被授权拦截。
「JWT 不生效」十有八九是自定义过滤器插错位置或请求匹配了另一条链。
八、常见误区与追问
- 误区:Spring Security 只有一条固定过滤器链。 应用可以定义多条
SecurityFilterChain,FilterChainProxy按 matcher 选择第一条匹配的链。 - 误区:自定义 JWT 过滤器放哪都可以。 过滤器必须放在授权之前完成认证写入,否则授权阶段仍会把请求当匿名处理。
- 误区:
permitAll和 ignoring 是一回事。permitAll仍走安全链,ignoring 直接跳过安全链,业务接口通常更适合permitAll。 - 追问:为什么异常处理过滤器要在授权过滤器前面? 它要捕获后续认证或授权异常,并转换成 401、403 或登录跳转。
- 追问:API 未认证返回 HTML 登录页通常是什么问题? API 链没有自定义
AuthenticationEntryPoint,被默认表单登录响应污染。 - 追问:排查匹配错链最直接的方法是什么? 打开 Spring Security DEBUG 日志,看请求命中了哪条链和经过了哪些过滤器。
九、加强记忆
Spring Security 核心是 Servlet Filter 链:DelegatingFilterProxy(容器 Filter)→ FilterChainProxy(Spring 总入口)→ 按 matcher 选中一条 SecurityFilterChain → 依次执行安全过滤器。链上主线顺序:准备 SecurityContext → 认证过滤器(提取凭证、交 AuthenticationManager 校验、成功存入 SecurityContext)→ ExceptionTranslationFilter(认证失败→401/EntryPoint、权限不足→403/AccessDeniedHandler)→ AuthorizationFilter(授权决策,末端)。顺序关键(上下文先备、认证在授权前、异常处理包住后续)。可有多条链用 matcher + @Order 区分(具体规则放前面)。permitAll(走链只是放行)≠ ignoring(跳过整条链,无上下文/安全头)——业务接口用 permitAll、静态资源可 ignoring。排查用 debug 日志看匹配哪条链、过滤器顺序、自定义过滤器插入位置。