《Spring Boot 高级》8.1 过滤器链构建过程

从 DelegatingFilterProxy 的延迟查找讲到 FilterChainProxy 的匹配:拆开 WebSecurityConfiguration 产出的 springSecurityFilterChain bean、HttpSecurity DSL 如何装配过滤器、FilterOrderRegistration 的排序规则,以及多条 SecurityFilterChain 的选取。

本节目标:讲清 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_ORDER2147483642独立 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 的流程是:

  1. 遍历所有 SecurityFilterChain,取第一个 matches(request) 返回 true 的链;
  2. 把该链的 getFilters() 装进内部类 VirtualFilterChain;
  3. 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(...)AuthorizeHttpRequestsConfigurerAuthorizationFilter
formLogin(...)FormLoginConfigurerUsernamePasswordAuthenticationFilter
securityContext(...)SecurityContextConfigurerSecurityContextHolderFilter
csrf(...)CsrfConfigurerCsrfFilter
headers(...)HeadersConfigurerHeaderWriterFilter
anonymous(...)AnonymousConfigurerAnonymousAuthenticationFilter

构建分三步(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,故下面的行号即权威次序):

次序过滤器职责
1DisableEncodeUrlFilterURL 重写防护
2ForceEagerSessionCreationFilter强制提前建 session
3SecurityContextHolderFilter从 SecurityContextRepository 载入上下文
4HeaderWriterFilter写安全响应头
5CsrfFilterCSRF 校验
6LogoutFilter登出处理
7UsernamePasswordAuthenticationFilter表单登录认证
8RequestCacheAwareFilter恢复被缓存的原请求
9RememberMeAuthenticationFilterRemember-Me 自动认证
10AnonymousAuthenticationFilter兜底匿名身份
11SessionManagementFilter会话管理
12ExceptionTranslationFilter把认证/授权异常翻译成 401/403
13AuthorizationFilter授权决策入口

这张表解释了两个常见现象:为什么 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 认证与授权流程 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计