如果你在项目里折腾过 Spring Security一定见过这两个名字WebSecurity和HttpSecurity。官方文档里能查到HttpSecurity怎么配置但一旦问到FilterChainProxy这个总入口是怎么被WebSecurity拼出来的资料就明显变少了。我第一次把断点打到WebSecurity.performBuild()上时其实是为了查一个线上现象项目里定义了两条SecurityFilterChain一条只处理/internal/**一条兜底处理所有请求结果/internal/**的请求经常没有走进我以为的那条链。看了半天日志才发现问题根本不在所谓的“过滤器没生效”而是出在 Spring Security 对多条链的顺序选择方式上。要讲清楚这件事就得回到源头WebSecurity到底是怎么构建出FilterChainProxy的。这个问题对两类人特别有用一类是维护老项目、准备从WebSecurityConfigurerAdapter迁到新写法的人另一类是已经在用 Spring Boot 3 / Spring Security 6但在多SecurityFilterChain场景里遇到“路径匹配不对、过滤器不执行、静态资源被安全策略误伤”的人。下面我会把源码主线、注册链路、匹配顺序、调试方法串起来讲代码只挑关键片段不做全文贴源码式的流水账。1. 先理顺概念WebSecurity、HttpSecurity、FilterChainProxy到底谁套谁很多初学者会把WebSecurity和HttpSecurity当成同一个东西其实它们的职责边界非常清楚HttpSecurity是“某一组接口”的安全配置器。它最终生成一个SecurityFilterChain,里面包含一串安全过滤器。WebSecurity是所有SecurityFilterChain的顶层组装器。它最终生成的不是一个普通的过滤器而是一个FilterChainProxy。FilterChainProxy是把这个过滤器链真正暴露到 Servlet 容器里的入口。它自己不处理认证也不处理授权只负责“这请求该走哪条链”然后把请求交给对应链上的过滤器们。1.1 用“总配电箱 分支电路”来帮助理解可以直接把FilterChainProxy想成一个总配电箱每个SecurityFilterChain就是总配电箱下面的一路分支。HttpSecurity则负责一路分支里的具体电路怎么接先接UsernamePasswordAuthenticationFilter再接过ExceptionTranslationFilter再接AuthorizationFilter。WebSecurity做的事情是把这一路一路的分支电路都收集好装进总配电箱最后给总配电箱一个统一的对外接口也就是FilterChainProxy。请求进入时FilterChainProxy拿着请求去逐个问每一路分支“你匹配这个请求吗”匹配上的那一路就执行没有匹配的路就直接跳过。如果一路都没匹配请求也不会 403而是不带 Spring Security 的过滤器直接往后走。这个问题很容易被忽略后面我会用一个案例具体说明。1.2 “Proxy”这个后缀为什么不是白叫的FilterChainProxy名字里的 Proxy 容易让人误解好像它只是某个安全过滤器的代理。其实不是。它代理的是整个安全过滤器集合也可以说它的核心数据结构就是一个ListSecurityFilterChain。SecurityFilterChain接口本身非常简单public interface SecurityFilterChain { boolean matches(HttpServletRequest request); ListFilter getFilters(); }换句话说过滤链的本质就是一个“匹配器 过滤器列表”。FilterChainProxy的全部核心逻辑就是遍历这个List找到第一个matches(request)返回 true 的链然后把请求交给这条链的getFilters()。所以分析WebSecurity构建FilterChainProxy的逻辑最核心的问题就变成WebSecurity是怎么把各种来源的SecurityFilterChain汇集到同一个列表里的这个列表又是按什么顺序排列的这两个问题解决了Spring Security 的启动模型你就懂了大半。2. WebSecurity不是自己写了个Filter而是组合了一个路由表performBuild核心逻辑WebSecurity本身继承自AbstractConfiguredSecurityBuilderFilter, WebSecurity。这个底层 Builder 的生命周期和普通对象不太一样它有几个固定阶段init()、configure()、performBuild()。WebSecurity.build()被调用之后并不会直接执行组装动作而是先把所有挂在它身上的SecurityConfigurer先init、再configure最后才轮到真正生成对象的performBuild。2.1 三段式生命周期里藏着关键顺序问题在代码里你可以看到build()最终会调用到doBuild()抽象流程简化后大概是这个样子// AbstractConfiguredSecurityBuilder 中 private void doBuild() throws Exception { init(); configure(); performBuild(); }这里的顺序非常重要。WebSecurity上可能挂了多个SecurityConfigurer这些 Configurer 的init()和configure()方法做的事情不一样。有的 Configurer 会往WebSecurity的某个列表里塞数据有的会把一个HttpSecurity也启动起来。performBuild()只能拿到前面两个阶段都执行完之后的最终状态。所以你会发现WebSecurity的构建逻辑不能只盯着performBuild()一段看还要看init()和configure()阶段往里攒了什么。真正在performBuild()里做的其实就三件事处理WebSecurity.ignoring()登记的忽略路径。调每一个HttpSecurity构建出的SecurityFilterChain。把这些链都打包进FilterChainProxy。2.2 核心代码走查ignoring优先还是SecurityFilterChain优先以 Spring Security 6 的源码为例WebSecurity.performBuild()的简化版长这样Override protected Filter performBuild() throws Exception { ListSecurityFilterChain securityFilterChains new ArrayList(); // 1. WebSecurity.ignoring() 登记过的请求会转成空过滤器链 for (RequestMatcher ignoredRequest : this.ignoredRequests) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); } // 2. 构建 HttpSecurity 阶段生成的 SecurityFilterChain for (SecurityBuilder? extends SecurityFilterChain securityFilterChainBuilder : this.securityFilterChainBuilders) { SecurityFilterChain chain securityFilterChainBuilder.build(); securityFilterChains.add(chain); } // 3. 把所有链交给 FilterChainProxy FilterChainProxy filterChainProxy new FilterChainProxy(securityFilterChains); filterChainProxy.afterPropertiesSet(); return filterChainProxy; }不同小版本的字段名和收集方式会有差异Spring Security 5.x 里你可能是通过WebSecurityConfigurerAdapter来间接塞 builder6.x 里直接把HttpSecurity.build()产生的SecurityFilterChain收集到容器里。但落到performBuild()这一步主干逻辑没有变过先加ignoredRequests再加securityFilterChainBuilders。这个顺序直接影响请求匹配。因为FilterChainProxy后面的匹配逻辑是“谁先匹配成功就走谁”所以ignoredRequests排在最前等价于告诉 Spring Security这些路径只要满足忽略匹配就直接走一条“没有任何安全过滤器”的空链后面的安全策略一概不参与。2.3 “空过滤器链”到底算不算一条链注意上面代码里的new DefaultSecurityFilterChain(ignoredRequest)。很多人以为忽略路径就是不在FilterChainProxy里出现其实不是忽略路径也会生成一个SecurityFilterChain只是它的filters是空列表。空链在FilterChainProxy里的效果和“完全没有匹配的链”不一样。空链匹配之后请求不会执行任何安全过滤器而是直接往下走到原始FilterChain。如果一条都没匹配上请求也会继续往下走但语义上就不是“你故意忽略了它”而是“Spring Security 不知道该怎么处理它”。这两种情况在排障时绝对不能混为一谈。WebSecurity.ignoring()是“主动且优先地不处理”后续的所有安全链都不会执行。而你如果只是因为securityMatcher写得不全导致没匹配到链那结果是“默认没人管”等哪天另一个匹配规则覆盖到这个路径行为又会突然改变。后面第五部分我会讲怎么用日志验证到底是哪种情况。3. 真正执行的一刻FilterChainProxy是怎么把请求交给一条链的WebSecurity.build()返回的是一个Filter对象但这个对象的名字叫springSecurityFilterChain。在 Spring Boot 应用中Servlet 容器实际注册的并不是FilterChainProxy本体而是 Spring 的DelegatingFilterProxy。3.1 为什么容器里注册的是springSecurityFilterChain而不是FilterChainProxy老项目如果不用 Spring Boot会通过AbstractSecurityWebApplicationInitializer往 Servlet 容器里注册一个DelegatingFilterProxytargetBeanName 默认就是springSecurityFilterChain。Spring Boot 的做法也类似SecurityFilterAutoConfiguration里会发现容器中是否存在名为springSecurityFilterChain的 Bean如果存在就注册一个DelegatingFilterProxyRegistrationBean这个注册 Bean 会把请求代理给真正的springSecurityFilterChainBean。WebSecurityConfiguration里就有这样一个 Bean 方法Bean(name springSecurityFilterChain) public Filter springSecurityFilterChain() throws Exception { return this.webSecurity.build(); }所以你在源码里搜springSecurityFilterChain这个 Bean 名时最后拿到的对象其实是WebSecurity.build()的产物也就是FilterChainProxy。这种设计最大的好处是解耦。Servlet 容器在初始化时只需要注册一个简单的代理过滤器FilterChainProxy本身什么时候构建、里面有多少条链、链上的过滤器怎么编排全部由 Spring 上下文的生命周期管理。你在应用启动日志里看到 “springSecurityFilterChain” 这个过滤器名时也不要觉得奇怪它的真实类型打印出来通常就是org.springframework.security.web.FilterChainProxy。3.2 FilterChainProxy是怎么找到第一条匹配链的进入FilterChainProxy.doFilter()后它会先把当前请求标记为已经被过滤器处理过然后调用一个内部的doFilterInternal()。核心逻辑其实非常朴素private void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // ...省略很多 trace 日志 ListFilter filters getFilters(request); if (filters null || filters.isEmpty()) { // 没有任何链匹配或者空链 chain.doFilter(request, response); return; } VirtualFilterChain virtualFilterChain new VirtualFilterChain(chain, filters); virtualFilterChain.doFilter(request, response); } private ListFilter getFilters(HttpServletRequest request) { for (SecurityFilterChain securityFilterChain : this.filterChains) { if (securityFilterChain.matches(request)) { return securityFilterChain.getFilters(); } } return null; }注意看循环它一旦遇到第一个matches(request)返回 true 的链就直接返回这条链的过滤器列表了。它不是先收集所有匹配的链也不是选一个“最匹配”的链也没有“最长的路径优先”这种后来居上的逻辑。顺序就是绝对的“先到先得”。所以在多条链的设计里链的顺序不是锦上添花而是正确性的核心。如果兜底的AnyRequest链排在/api/**链前面那么/api/**的请求永远先被兜底链吃掉后面的apiFilterChain根本不会执行你在后一条链里配的 JWT 过滤器自然一个都不会出现。3.3 VirtualFilterChain链上过滤器怎么完成接力找到当前链的过滤器列表后FilterChainProxy会把这些过滤器