本节目标:讲清
springSecurityFilterChain这个Filter从「Spring bean」到「容器里的过滤器」的两段式旅程,拆开FilterChainProxy的链匹配与HttpSecurity的 DSL 装配,给出关键过滤器的权威顺序与验证方法。
适用版本:Spring Boot 4.1.x(Java 21)
8.1 过滤器链构建过程
实战卷解决的是「怎么配」——authorizeHttpRequests 怎么写、CSRF 怎么关。本节解决「为什么这样配」:你写下的 .authorizeHttpRequests(...) 究竟变成了哪个对象、它又是在什么时候被塞进 Servlet 容器的过滤器列表里的。
先把结论放前面:Spring Security 的过滤器链根本不在你写的 SecurityFilterChain bean 里,而在容器里的一个 DelegatingFilterProxy。这个 proxy 在第一个请求到达时才去 Spring 容器里取名叫 springSecurityFilterChain 的 bean,那个 bean 才是真正的 FilterChainProxy。搞清这条「bean → 容器过滤器」的桥,@Order 不生效、addFilterBefore 找不到参照类、securityMatcher 匹配不上这些问题才有地方下手。
本章三节沿用同一个图书借阅系统:角色 ROLE_READER(读者)与 ROLE_LIBRARIAN(馆员),接口 /books/**、/loans、/admin/**。本节先讲链路骨架,8.2 讲认证与授权决策,8.3 讲方法安全与线程传播。
8.1.1 两段式:bean 定义与容器注册是两个阶段
第一段发生在 Spring 容器里。WebSecurityConfiguration(org.springframework.security.config.annotation.web.configuration,来自 spring-security-config)声明了一个 bean:
public jakarta.servlet.Filter springSecurityFilterChain(
ObjectProvider<HttpSecurity> httpSecurity) throws Exception;
返回类型是 jakarta.servlet.Filter,实际类型是 FilterChainProxy(两者均已从本机 spring-security-config-7.1.1.jar 核实)。名字固定为 springSecurityFilterChain。
第二段发生在 Servlet 容器里,由 Spring Boot 的自动配置完成。SecurityFilterAutoConfiguration 注册一个 DelegatingFilterProxyRegistrationBean:
DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityFilterProperties securityFilterProperties);
这个注册 bean 做三件事:把目标名设为 springSecurityFilterChain,把 order 设为 SecurityFilterProperties.DEFAULT_FILTER_ORDER,再把 DelegatingFilterProxy 注册进 ServletContext。核实到的常量值:
| 常量 | 值 | 含义 |
|---|---|---|
SecurityFilterProperties.DEFAULT_FILTER_ORDER | -100 | 安全过滤器的默认 order |
SecurityFilterProperties.BASIC_AUTH_ORDER | 2147483642 | 独立 Basic 认证过滤器的 order |
两条路都指向同一个 bean 名,但那个 bean 本身是 FilterChainProxy,它的构造需要一个 List<SecurityFilterChain>。这个列表由 WebSecurityConfiguration 收集:该类有一个 setFilterChains(List<SecurityFilterChain>) 方法(核实签名 void setFilterChains(List<SecurityFilterChain>)),把容器里所有 SecurityFilterChain bean 注入进来,再交给 springSecurityFilterChain() 构造 FilterChainProxy。所以「我定义了链,框架怎么知道」的答案就在这里——不是扫描出来的,是显式注入的。
一句话概括这段关系:
@Bean SecurityFilterChain → WebSecurityConfiguration.setFilterChains(List)
→ springSecurityFilterChain() → new FilterChainProxy(chains)
→ DelegatingFilterProxyRegistrationBean(目标名=springSecurityFilterChain)
→ ServletContext.addFilter(DelegatingFilterProxy)
4.x 的模块化变化必须记牢:3.x 时 SecurityFilterAutoConfiguration 在 spring-boot-autoconfigure 的 org.springframework.boot.autoconfigure.security.servlet 包下;4.x 迁到了独立模块 spring-boot-security,包名变成 org.springframework.boot.security.autoconfigure.web.servlet。这正是 BRIEF 说的「模块名 spring-boot-<technology>、根包 org.springframework.boot.<technology>」的一个实例。你若要自定义 securityFilterChainRegistration 的 order,用属性 spring.security.filter.order(对应 SecurityFilterProperties.getOrder())覆盖。
8.1.2 DelegatingFilterProxy:为什么不能直接注册 FilterChainProxy
一个自然的疑问是:既然 springSecurityFilterChain 已经是个 Filter bean,为什么还要包一层 DelegatingFilterProxy?
因为 Servlet 容器的生命周期和 Spring 容器不同步。ServletContext.addFilter(...) 发生在 Web 服务器启动早期,此时 Spring 容器可能还没 refresh 完,springSecurityFilterChain bean 甚至还没被创建。DelegatingFilterProxy(org.springframework.web.filter.DelegatingFilterProxy,来自 spring-web)解决的正是这个时序问题——它继承 GenericFilterBean,把「找到真正的 Filter」推迟到第一次 doFilter 或 initFilterBean:
| 方法 | 作用 |
|---|---|
setTargetBeanName(String) / getTargetBeanName() | 设定要查找的 bean 名 |
findWebApplicationContext() | 从 ServletContext 属性里拿到 WebApplicationContext |
initDelegate(WebApplicationContext) | 首次解析目标 bean 并缓存为 delegate |
invokeDelegate(Filter, ...) | 把请求转发给 delegate |
关键点:DelegatingFilterProxy 自己不含任何安全逻辑,它只是「容器世界」到「Spring bean 世界」的桥。所以你在排查过滤器问题时,断点应该下在 FilterChainProxy.doFilter,而不是 DelegatingFilterProxy.doFilter——后者只是转发。
8.1.3 FilterChainProxy 与 VirtualFilterChain
真正的过滤器调度在 FilterChainProxy(org.springframework.security.web.FilterChainProxy extends GenericFilterBean)。它持有一组 SecurityFilterChain,构造器签名是 FilterChainProxy(List<SecurityFilterChain>)。
SecurityFilterChain 是个只有两个方法的接口(核实自 spring-security-web-7.1.1.jar):
public interface SecurityFilterChain {
boolean matches(HttpServletRequest request);
List<Filter> getFilters();
}
最常用的实现是 DefaultSecurityFilterChain,它把 RequestMatcher 和 List<Filter> 组合起来。FilterChainProxy.doFilter 的流程是:
- 遍历所有
SecurityFilterChain,取第一个matches(request)返回true的链; - 把该链的
getFilters()装进内部类VirtualFilterChain; VirtualFilterChain.doFilter逐个调用过滤器的doFilter,走完后调用原始FilterChain(最终到DispatcherServlet)。
FilterChainProxy 还提供 getFilters(String)(按 URL 取链)与 getFilterChains(),这两个方法在验证时非常有用。注意 VirtualFilterChain 是一个「虚拟链」:它把 List<Filter> 包装成标准的 jakarta.servlet.FilterChain,让每个过滤器都能用同样的 chain.doFilter(req, resp) 写法推进——这正是「责任链」模式在 Servlet 世界的落地方式。
8.1.4 HttpSecurity DSL 如何装配出一串过滤器
HttpSecurity 是链的装配器,它的类签名(核实自 spring-security-config-7.1.1.jar)是:
public final class HttpSecurity
extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
implements HttpSecurityBuilder<HttpSecurity> { ... }
每个 http.xxx(Customizer) 方法注册一个 SecurityConfigurer,而不是直接 addFilter。例如:
| DSL 方法 | 注册的 configurer | 最终产出的过滤器 |
|---|---|---|
authorizeHttpRequests(...) | AuthorizeHttpRequestsConfigurer | AuthorizationFilter |
formLogin(...) | FormLoginConfigurer | UsernamePasswordAuthenticationFilter |
securityContext(...) | SecurityContextConfigurer | SecurityContextHolderFilter |
csrf(...) | CsrfConfigurer | CsrfFilter |
headers(...) | HeadersConfigurer | HeaderWriterFilter |
anonymous(...) | AnonymousConfigurer | AnonymousAuthenticationFilter |
构建分三步(AbstractConfiguredSecurityBuilder 的模板方法):先 init() 所有 configurer,再 configure() 它们(此时它们往 HttpSecurity 里塞过滤器),最后 performBuild() 返回 DefaultSecurityFilterChain。核实到 HttpSecurity.performBuild() 的返回类型正是 DefaultSecurityFilterChain。
所以 .build() 拿到的是「一个 RequestMatcher + 一串 Filter」,它随后被 WebSecurityConfiguration 收集进 FilterChainProxy 的构造参数里。整个装配链是:
http.authorizeHttpRequests(...).build()
└─> DefaultSecurityFilterChain(matcher, filters)
└─> WebSecurityConfiguration 收集所有链
└─> FilterChainProxy(List<SecurityFilterChain>)
└─> bean "springSecurityFilterChain"
└─> DelegatingFilterProxy(容器侧)
8.1.5 过滤器顺序:FilterOrderRegistration
DefaultSecurityFilterChain 里的 List<Filter> 本身没有顺序概念,顺序由 FilterOrderRegistration(package-private 类,org.springframework.security.config.annotation.web.builders)统一分配。它的方法只有两个:
void put(Class<? extends Filter> filter, int order);
Integer getOrder(Class<?> filter);
HttpSecurity.addFilterBefore/After/At 就是基于这张登记表算 offset 的。HttpSecurity 内部用 HttpSecurity$OrderedFilter(核实存在)承载「过滤器 + order」这一对。
登记表在静态初始化块里构造:内部类 FilterOrderRegistration$Step 从 100 起、每次 next() 递增 100,所以每个登记过的过滤器都有唯一且留有空隙的序号——空隙正是留给 addFilterBefore/After 插入自定义过滤器的。put 给某个类一个绝对序号,getOrder 则用来解析 Before/After 的参照类;参照类若没登记过,getOrder 返回 null,HttpSecurity 就会抛「找不到 order」的异常。
下面是从 FilterOrderRegistration 构造函数字节码里读出的关键过滤器相对顺序(Step.next() 从 100 起每次 +100,故下面的行号即权威次序):
| 次序 | 过滤器 | 职责 |
|---|---|---|
| 1 | DisableEncodeUrlFilter | URL 重写防护 |
| 2 | ForceEagerSessionCreationFilter | 强制提前建 session |
| 3 | SecurityContextHolderFilter | 从 SecurityContextRepository 载入上下文 |
| 4 | HeaderWriterFilter | 写安全响应头 |
| 5 | CsrfFilter | CSRF 校验 |
| 6 | LogoutFilter | 登出处理 |
| 7 | UsernamePasswordAuthenticationFilter | 表单登录认证 |
| 8 | RequestCacheAwareFilter | 恢复被缓存的原请求 |
| 9 | RememberMeAuthenticationFilter | Remember-Me 自动认证 |
| 10 | AnonymousAuthenticationFilter | 兜底匿名身份 |
| 11 | SessionManagementFilter | 会话管理 |
| 12 | ExceptionTranslationFilter | 把认证/授权异常翻译成 401/403 |
| 13 | AuthorizationFilter | 授权决策入口 |
这张表解释了两个常见现象:为什么 AuthorizationFilter 一定在 UsernamePasswordAuthenticationFilter 之后(认证必须先于授权),为什么 ExceptionTranslationFilter 紧贴在 AuthorizationFilter 之前(它要 catch 后者抛出的 AccessDeniedException)。注意登记表里 SecurityContextPersistenceFilter 排在 SecurityContextHolderFilter 之后且已标记 @Deprecated(从 javap -v 看到 Deprecated: true),新代码一律用后者。
8.1.6 多条链的匹配与顺序
当你有多个 SecurityFilterChain bean(比如 /api/** 走无状态、其余走表单登录),FilterChainProxy 按 bean 的 @Order 从小到大遍历,取第一个匹配的链,命中即停。所以「越具体的链要排越前」:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().hasRole("READER"))
.build();
}
@Bean
@Order(2)
SecurityFilterChain webChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("LIBRARIAN")
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.build();
}
}
如果两条链的 securityMatcher 都匹配某个请求,@Order 小的先赢;如果顺序写反(宽链在前),窄链永远匹配不到——这是「我加了链但没生效」的头号原因。此外 @Order 相同的多条链会抛错,Hugo 站点里另一类常见错误是链没写 securityMatcher(默认匹配所有),把后面的链全遮住。
8.1.7 自定义过滤器与插入点
除了 DSL 自带的过滤器,你常需要插入自己的过滤器(比如记录访问日志、校验自定义请求头)。HttpSecurity 的四个方法(均已核实)是唯一入口:
| 方法 | 语义 | 参照类要求 |
|---|---|---|
addFilter(Filter) | 按过滤器自身登记的 order 插入 | 必须能被 FilterOrderRegistration 找到 |
addFilterBefore(Filter, Class) | 插在参照类之前 | 参照类必须登记过 |
addFilterAfter(Filter, Class) | 插在参照类之后 | 参照类必须登记过 |
addFilterAt(Filter, Class) | 占用参照类的 order 位 | 参照类必须登记过 |
例如「在授权之前记录将要访问的路径」:
@Bean
SecurityFilterChain webChain(HttpSecurity http) throws Exception {
http.addFilterBefore(new AccessLogFilter(), AuthorizationFilter.class);
return http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.build();
}
这里 AuthorizationFilter.class 是合法的参照类,因为它已在 FilterOrderRegistration 里登记(见 8.1.5 的顺序表)。若换成 AccessLogFilter.class 自身当参照,会直接抛异常——这就是「锚点必须用框架认识的类」的含义。
注意 addFilterBefore 插入的过滤器不会自动获得「每请求执行一次」等语义,也不会被调试日志特别标注;它的位置只由算出的 order 决定。
8.1.8 知道之后能做什么
打印真实链。 把 FilterChainProxy 注入进来,在启动后 dump 每条链的过滤器类名,顺序一目了然(方法均已核实):
@Component
class FilterChainDumper implements ApplicationRunner {
private final FilterChainProxy proxy;
FilterChainDumper(FilterChainProxy proxy) {
this.proxy = proxy;
}
@Override
public void run(ApplicationArguments args) {
proxy.getFilterChains().forEach(chain ->
System.out.println("chain=" + chain + " -> " +
chain.getFilters().stream()
.map(f -> f.getClass().getSimpleName())
.toList()));
}
}
开调试模式。 @EnableWebSecurity(debug = true)(核实 EnableWebSecurity.debug() 存在)会在每个请求打出一条完整的过滤器链日志,能看到实际参与本次请求的过滤器与它们的顺序。生产环境务必关掉,否则日志量巨大。
自定义插入点。 HttpSecurity 提供 addFilter、addFilterBefore、addFilterAfter、addFilterAt(均已核实)。Before/After 的参照类必须在 FilterOrderRegistration 登记过,否则会抛「找不到 order」的异常——这就是为什么不能拿一个随便的类当锚点。
排障清单。 401 而非 403:ExceptionTranslationFilter 判定为「未认证」,问题在认证环节;403 而非 401:认证过了但授权没过,问题在 AuthorizationFilter 的决策;过滤器「没执行」:先确认它落在哪条链、那条链有没有被更靠前的宽链遮住。
小结
springSecurityFilterChain经历「Spring bean(FilterChainProxy)→ 容器过滤器(DelegatingFilterProxy)」两段式;4.x 的自动配置在spring-boot-security模块的org.springframework.boot.security.autoconfigure.web.servlet包。FilterChainProxy遍历链取第一个matches命中的,用VirtualFilterChain串起该链的过滤器。HttpSecurity是 DSL 装配器,每个xxx(...)注册一个 configurer,performBuild()产出DefaultSecurityFilterChain。- 顺序由
FilterOrderRegistration统一分配;认证过滤器在前、授权过滤器在后、异常翻译过滤器紧贴授权过滤器之前。 - 多条链按
@Order先匹配先赢,具体链必须排在宽链之前。
下一节进入认证与授权本身:AuthenticationManager 如何挑选 AuthenticationProvider、SecurityContext 如何在请求间存活、AuthorizationManager 又是在哪一刻做出放行或拒绝的决定。
阅读导航:上一节:7.3 并发编程与线程池模型 · 下一节:8.2 认证与授权流程 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。