尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Security过滤器链注入Tomcat的完整流程解析

Spring Security过滤器链注入Tomcat的完整流程解析 如果你在基于 Spring Boot 的项目里单步调试过一次请求大概率会有这种感觉请求明明只是访问一个普通的 Controller调用栈里却先出现了 Tomcat 的 ApplicationFilterChain然后又出现了 Spring Security 的 FilterChainProxy最后才轮到 DispatcherServlet。Spring Security 最终是以 Filter 的身份挂到 Tomcat 上的这一点很多人知道但它不是一个简单的 Filter——它内部还有一条完整的过滤器链中间还隔着一个 DelegatingFilterProxy 负责牵线。这篇文章我就从源码角度把 Spring Security 注入 Tomcat Filter 链这件事分成启动和请求两个阶段完整过一遍。文章以 Spring Boot 3 Spring Security 6 内嵌 Tomcat 为例适合那些想彻底搞懂过滤链匹配、过滤器顺序、认证过滤器执行过程的 Java 后端开发者。1. 先搞明白 Tomcat 的 FilterChain 是怎么串起来的在追 Spring Security 源码之前必须先回到 Servlet 规范。不管 Spring Security 内部包装得多复杂它最终面对的还是 Tomcat 的过滤器模型所以这一节先把这个地基讲清楚。1.1 一次 HTTP 请求在 Tomcat 里经过的原始路径Servlet 规范把 Filter 定义成一个非常朴素的接口public interface Filter { default void init(FilterConfig filterConfig) throws ServletException {} void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; default void destroy() {} }FilterChain 又是什么它就是一个按顺序串起来的过滤器队列。Tomcat 内部维护一个 ApplicationFilterChain核心结构可以理解成三个字段一个 Filter 数组、一个当前指针、一个过滤器总数。每次调用chain.doFilter(request, response)内部就会把指针加 1然后调用下一个过滤器的doFilter方法。当指针走完整个数组最后落到 Servlet 的service方法上请求才真正进入 Spring MVC 的 DispatcherServlet。这个 ApplicationFilterChain 不是在请求刚进 Tomcat 时创建的而是经过了四级容器之后才组装。请求从 Connector 进来依次经过 Engine、Host、Context、Wrapper走到最内层的 StandardWrapperValve 时Tomcat 会根据当前 Context 里维护的 FilterMap 找出和请求路径匹配的过滤器把它们整理成 ApplicationFilterChain然后才启动这个链条。FilterMap 的匹配规则比较细包括 URL pattern、Servlet 名、DispatcherType 等。Filter 的执行顺序也不是单纯按照注册顺序来的它会受到 FilterOrdering 的影响多个 Filter 之间还存在一个按类名/名字排序的过程。这块如果你没有实际遇到过多个框架过滤器互相打架的问题可能注意不到但它是后续理解 Spring Security 过滤器顺序的一个重要背景。1.2 为什么 Spring Security 不依赖 Spring MVC 的拦截器很多刚接触 Spring Security 的人会有个疑问Spring Boot 本身就是 Spring 生态Spring MVC 也有 Interceptor 机制为什么 Spring Security 非要采用 Servlet 的 Filter答案是执行时机和覆盖范围。Spring MVC 的 Interceptor 是在请求已经进入 DispatcherServlet 之后通过 HandlerMapping 找到对应 HandlerExecutionChain 才被调用的。这个阶段相对靠后很多请求如果根本匹配不到 Handler或者请求的是静态资源拦截器不一定能覆盖到。Filter 则不一样它在 Servlet 容器层面就挂载好了请求还没到 DispatcherServlet 就已经被处理可以让安全控制覆盖到所有经过容器的请求。但 Spring Security 并没有把一大堆安全逻辑全部塞进一个 Filter 里。那样做会很臃肿可维护性极差。它的思路是对外只暴露一个 Filter内部再维护一条由许多不同职责的小过滤器组成的“链中链”。这样一来容器只认识一个入口Spring Security 内部又可以保持高度的模块化。所以你去看调用栈时会发现有两层 FilterChain外层是 Tomcat 的 ApplicationFilterChain内层是 Spring Security 的 VirtualFilterChain。2. 启动阶段Spring Boot 自动配置是如何“塞”过滤器给 Tomcat 的到了启动阶段问题可以拆成三个小问题自动配置类里到底发生了什么注册的过滤器叫什么名字它是一个真实过滤器还是个壳Tomcat 启动时是怎么看到这个过滤器的2.1 自动配置的入口SecurityFilterAutoConfiguration 都干了什么Spring Boot 在类路径里检测到 Spring Security 相关依赖后会加载两个关键自动配置类。SecurityAutoConfiguration 负责把 Spring Security 的配置体系引入 Spring 容器比如导入 SpringBootWebSecurityConfiguration、UserDetailsServiceAutoConfiguration 等。它解决的问题是“Spring Security 的 Bean 怎么创建、怎么组合”。真正和 Servlet 容器关联起来的是另一个类 SecurityFilterAutoConfiguration。SecurityFilterAutoConfiguration 的核心逻辑可以简化成下面这样Bean ConditionalOnMissingBean(name DelegatingFilterProxyRegistrationBean.DEFAULT_FILTER_NAME) ConditionalOnBean(name springSecurityFilterChain) public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties properties) { DelegatingFilterProxyRegistrationBean registration new DelegatingFilterProxyRegistrationBean(springSecurityFilterChain); registration.setOrder(properties.getFilter().getOrder()); return registration; }这里面有几个点值得注意。ConditionalOnBean(name springSecurityFilterChain)表示必须先存在一个名为springSecurityFilterChain的 Bean这个 Bean 是在 WebSecurityConfiguration 中创建的实际类型是 FilterChainProxy。也就是说自动配置注册的是“去调用 springSecurityFilterChain 这个 Bean”的注册器而不是直接注册一个正在运行的安全过滤链对象。DelegatingFilterProxyRegistrationBean的默认order值是SecurityProperties.DEFAULT_FILTER_ORDER也就是 -100。这表示它希望尽量在 Tomcat 过滤链中靠前执行但又不是最前要给一些全局编码、监控类的过滤器留出空间。2.2 DelegatingFilterProxyRegistrationBean注册的其实是一个“壳”DelegatingFilterProxyRegistrationBean 的继承关系很值得讲一下。它继承自 AbstractFilterRegistrationBean而 AbstractFilterRegistrationBean 又实现了 ServletContextInitializer。这意味着 Spring Boot 的内嵌 Servlet 容器启动时会把这个注册器当作一个初始化回调来执行。它和普通 FilterRegistrationBean 的区别在于它注册的 Filter 是一个 Delegate 型过滤器构造时传入的不是一个已经实例化的 Filter 对象而是一个字符串targetBeanName。在 Spring Security 这个场景里targetBeanName就是springSecurityFilterChain。这里面最精妙的设计是“延迟查找”。DelegatingFilterProxy 在注册到 Tomcat 时并不会立刻去 Spring 容器里找那个 Bean而是等到容器启动完成、甚至到第一次请求进来时才去查找。为什么要这么绕因为 Filter 的注册时机和 Spring 容器的刷新时机并不同步。Tomcat 启动早期就把过滤器接上了但此时 Spring 的 BeanFactory 可能还在初始化中如果代理在注册时就强行去getBean(springSecurityFilterChain)很可能会因为 Bean 还没创建完而报错。用一个壳先占住位置后面按需再查就从根上规避了初始化顺序问题。2.3 Tomcat 启动时是在哪儿拿到这些 Filter 的以 Spring Boot 内嵌 Tomcat 为例完整路径是这样的SpringApplication 启动后进入 ServletWebServerApplicationContext这个 ApplicationContext 本身实现了 ServletContextInitializer。TomcatServletWebServerFactory 在创建内嵌 Tomcat 时会把容器里所有 ServletContextInitializer 收集起来Tomcat 启动过程中逐个执行它们的onStartup方法。DelegatingFilterProxyRegistrationBean.onStartup一旦被执行就会把那个 DelegatingFilterProxy 添加到 ServletContext 里。Tomcat 侧对应的是 StandardContext 的addFilter方法内部最终会维护 FilterDefs 和 FilterMaps。FilterDefs 保存过滤器类名和初始化参数FilterMaps 保存这个过滤器匹配哪些 URL、哪些 DispatcherType。请求进来后ApplicationFilterChain 就是根据这两类数据去组链的。如果你用的是外置 Tomcat机制也差不多。Spring Boot 启动时通过 ServletContainerInitializer 机制也能触发同样的注册流程如果你还在用传统 web.xml 方式那就要手动配置一个 filter-class 为 DelegatingFilterProxy 的过滤器并且给一个targetBeanName的初始化参数值也是springSecurityFilterChain。核心思路完全一致都是先注册一个代理再由代理转接 Spring 容器的过滤器。3. 请求流转DelegatingFilterProxy 为何要挡在最前面到了请求阶段第一个真正被执行的 Spring Security 相关过滤器就是 DelegatingFilterProxy。这个名字里带 Proxy说明它不是干实事的它是桥梁。3.1 代理模式在 Filter 场景里的现实意义为什么 Spring Boot 不直接把 FilterChainProxy 注册到 Tomcat这里有一个生命周期管理权和依赖关系的问题。Servlet 容器里 Filter 的创建和销毁由容器负责Spring 容器里 Bean 的创建和销毁由 BeanFactory 负责。如果直接把 FilterChainProxy 这个 Spring Bean 塞给 TomcatTomcat 可以调用它的doFilter但无法管理它的初始化顺序也无法感知它在 Spring 容器里的依赖关系。反过来如果让 FilterChainProxy 直接实现 Filter 接口并在 ServletContext 注册Spring 容器一旦刷新重启这个 Filter 的状态就会变得很难维护。DelegatingFilterProxy 作为中间层解决了这个矛盾。它本身必须是容器管理的但它实例化之后只有两个动作从 Spring 容器里找 Bean然后把请求委托出去。你可以把它想成是一个转接插座墙上的接口必须要有但真正供电的电器可以晚点插上。插座阶段只保证规格兼容不关心背后是什么电器。3.2 init 阶段先建立“接头暗号”DelegatingFilterProxy 继承自 GenericFilterBean所以它的初始化入口是init()最终会调到一个可覆写的方法initFilterBean()。在这个方法里它主要做三件事从 WebApplicationContextUtils 里找到当前应用对应的 WebApplicationContext。通过targetBeanName调用wac.getBean(targetBeanName, Filter.class)拿到目标过滤器。如果配置了targetFilterLifecycle为 true还会额外调用目标过滤器的init方法。默认情况下这个值是 false意思是生命周期仍然由 Spring 容器管理容器不会把目标过滤器的 destroy 交给 Tomcat。为什么targetBeanName能找到因为在 WebSecurityConfiguration 里Spring Security 注册了一个名字正好是springSecurityFilterChain的 Bean声明类型是 Filter实际对象是 FilterChainProxy。DelegatingFilterProxy 只知道这个名字它不需要知道 FilterChainProxy 内部有多复杂它的任务就是找到它然后站到一边。3.3 doFilter 阶段每一次请求都从这里过doFilter的源码逻辑看起来不复杂但有一个很关键的延迟初始化判断。简化后的伪代码是这样Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) { Filter delegateToUse this.delegate; if (delegateToUse null) { synchronized (this.delegateMonitor) { delegateToUse this.delegate; if (delegateToUse null) { WebApplicationContext wac findWebApplicationContext(); if (wac null) { throw new IllegalStateException(No WebApplicationContext found); } delegateToUse initDelegate(wac); this.delegate delegateToUse; } } } invokeDelegate(delegateToUse, request, response, filterChain); }invokeDelegate内部就一句话delegate.doFilter(request, response, filterChain)。也就是说DelegatingFilterProxy 自己的过滤逻辑几乎为零它只负责把这一棒交给 FilterChainProxy。从请求的整个生命周期看这一步特别重要Tomcat 的 ApplicationFilterChain 已经走到了一个名为springSecurityFilterChain的 Filter但这个 Filter 是个外交官真正的安全过滤链在它背后。这也是很多人第一次看调用栈时会困惑的“为什么有两个 FilterChain”的原因。4. 真正的安全过滤链FilterChainProxy 如何匹配 SecurityFilterChain从这一层开始才算进入 Spring Security 的主场。4.1 FilterChainProxy 拿到请求后做了什么FilterChainProxy 同样实现了 Filter 接口但在它的doFilter中它不是一上来就执行安全过滤器。它的思路是先做“路由”再做“执行”。public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { ListFilter filters getFilters(request); if (filters null || filters.isEmpty()) { chain.doFilter(request, response); return; } VirtualFilterChain virtualFilterChain new VirtualFilterChain(chain, filters); virtualFilterChain.doFilter(request, response); }getFilters(request)会遍历 FilterChainProxy 内部持有的 ListSecurityFilterChain逐个调用matches(request)。找到第一个匹配的 SecurityFilterChain 后取出它上面的所有过滤器组装成一个 ListFilter然后传入 VirtualFilterChain 执行。这里有个容易被忽略的细节FilterChainProxy 会通过 request 属性做防重入标记。同一个请求在过滤链中只允许通过 FilterChainProxy 一次避免因为内置转发或者内部再次进入容器的过滤器链时发生递归调用。排查问题时如果看到控制台里重复执行了两次安全过滤链优先怀疑是不是在 Spring MVC 内部做了一次 forward 或 error 跳转同时过滤器的 dispatcherType 配置又包含了 FORWARD。4.2 SecurityFilterChain 的匹配机制requestMatcher 决定你走哪条链SecurityFilterChain 接口非常干净就两个方法public interface SecurityFilterChain { boolean matches(HttpServletRequest request); ListFilter getFilters(); }默认实现是 DefaultSecurityFilterChain核心字段就是 RequestMatcher requestMatcher 和 ListFilter filters。RequestMatcher 负责判断“这个请求是否归我管”ListFilter 是这条链上要执行的所有安全过滤器。很多项目里会配置多个 SecurityFilterChain比如 API 请求走一条链、页面请求走另一条链。此时顺序非常关键FilterChainProxy 是按照链的注册顺序从上到下匹配的一旦第一条链的matches返回 true后面的链就不会再被检查。一个非常常见的错误是第一条链配置的时候没有写securityMatcher(/api/**)导致它匹配了所有请求后面的链永远不生效。所以只要配置了多条链务必用Order指定优先级并且给每条链都画好边界。Bean Order(1) public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { http.securityMatcher(/api/**); // ... } Bean Order(2) public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception { // ... }匹配失败也不等于直接拒绝请求。如果在所有 SecurityFilterChain 中都找不到匹配项FilterChainProxy 会走chain.doFilter放行到后续的容器过滤器最终进入 DispatcherServlet。但对于没有匹配到安全过滤链的请求由于没有授权过滤器参与它实际上不会受到 Spring Security 的权限保护。所以看到“某些路径不需要认证也能访问”时不要急着怀疑安全配置写错先查这条请求到底匹配到了哪条链。4.3 链上到底有哪些过滤器默认顺序以 Spring Security 6 常见的表单登录配置为例一条默认的 SecurityFilterChain 上会挂很多过滤器。它们不是平白出现的而是在调用http.build()时由各个 Configurer 按顺序addFilter进去的。我列一个常用的顺序表顺序过滤器类核心职责1DisableEncodeUrlFilter防止 session id 被编码进 URL降低泄漏风险2WebAsyncManagerIntegrationFilter将 SecurityContext 绑定到异步请求的 WebAsyncManager3SecurityContextHolderFilter从 SecurityContextRepository 加载已有认证信息4HeaderWriterFilter向响应写入安全相关的 Header5CsrfFilter校验 CSRF Token6LogoutFilter处理登出逻辑7UsernamePasswordAuthenticationFilter处理表单登录请求8DefaultLoginPageGeneratingFilter未配置登录页时生成默认登录页9RequestCacheAwareFilter登录成功后恢复之前缓存的请求10SecurityContextHolderAwareRequestFilter包装请求让 request 支持 isUserInRole 等方法11AnonymousAuthenticationFilter没有认证信息时给请求一个匿名的 Authentication12ExceptionTranslationFilter捕获认证和授权异常转换成 401/403 或登录跳转13AuthorizationFilter基于 AuthorizationManager 做最终授权判断这套顺序里有几处是硬约束不能乱。比如 ExceptionTranslationFilter 必须放在 AuthorizationFilter 之前因为授权异常要靠它来捕获并翻译成对应响应AnonymousAuthenticationFilter 也要在授权过滤器之前运行否则匿名访问受保护资源时压根没有 Authentication 对象授权逻辑没法区分“未登录”和“已登录但权限不足”。5. 把两段旅程接起来一次请求的完整生命周期与调试经验启动和请求分开看容易懂但真正排查问题时需要把两段拼成一张完整的图。我习惯把它拆成清晰的阶段来记。5.1 从启动到请求把完整链路拼起来阶段一应用启动阶段。Spring Boot 启动后WebSecurityConfiguration 会构建名字为springSecurityFilterChain的 FilterChainProxy Bean。紧接着 SecurityFilterAutoConfiguration 创建一个 DelegatingFilterProxyRegistrationBean这个注册器知道目标 Bean 名但不知道也不关心 Bean 的具体行为。内嵌 Tomcat 启动过程中ServletWebServerApplicationContext 作为 ServletContextInitializer 被回调它把容器里所有的 RegistrationBean 都收集起来执行onStartup。Tomcat 的 StandardContext 因此注册了一个叫springSecurityFilterChain的容器过滤器并在 FilterMap 中保存了它的匹配规则和顺序。阶段二请求到达阶段。请求从 Connector 进入经过 Engine、Host、Context、Wrapper 四级容器到 StandardWrapperValve 时ApplicationFilterChain 根据 FilterMap 组装出外层的过滤器集合。Tomcat 的 ApplicationFilterChain 按顺序遍历过滤器当执行到那个名为springSecurityFilterChain的过滤器时实际调用的就是 DelegatingFilterProxy 的doFilter。它从 Spring 容器中找到 FilterChainProxy把请求和 FilterChain 一并委托过去。FilterChainProxy 遍历 SecurityFilterChain找到匹配的那条后创建 VirtualFilterChain然后逐个执行链上的安全过滤器。等 Spring Security 内部的过滤器全部执行完VirtualFilterChain 会调用原始chain.doFilter回到 Tomcat 的 ApplicationFilterChain继续执行后续的容器过滤器最终进入 DispatcherServlet。所以你在调用栈里看到两层过滤器链是完全正常的外层是 Tomcat 的 ApplicationFilterChain内层是 Spring Security 的 VirtualFilterChain。5.2 源码调试断点位置与观察变量如果遇到过滤链相关的问题我建议直接把断点打在下面这几个位置基本能快速定位问题想确认的事情断点位置观察什么过滤器是否注册成功DelegatingFilterProxyRegistrationBean.onStartuptargetBeanName、orderTomcat 外层过滤器链如何组成ApplicationFilterChain.doFilterfilters 数组、当前 pos代理是否成功委托DelegatingFilterProxy.doFilterdelegate 是否为非空请求匹配到哪条安全链FilterChainProxy.getFiltersfilterChains 数量、匹配到的 filters匹配规则是否符合预期DefaultSecurityFilterChain.matchesrequestMatcher 和 request.getRequestURI()内部过滤器执行顺序FilterChainProxy.VirtualFilterChain.doFilteradditionalFilters 数组、当前 currentPosition除了断点日志是一种更轻量的方式。开启 Spring Security 的调试日志后每个过滤器在处理请求时都会输出对应信息非常适合快速确认请求走到了哪一步logging.level.org.springframework.securityDEBUG logging.level.org.springframework.security.webDEBUGDEBUG 日志会输出很多细节比如AuthorizationFilter是否通过授权、CsrfFilter是否因为 token 缺失而拒绝了请求、某个路径是否匹配到了指定的 SecurityFilterChain 等。实际排查效率往往比打断点还高。5.3 实际项目中常见的三个坑坑 1自定义 Filter 加了 Component却没进入 Spring Security 链。把自定义 Filter 标注成Component后Spring Boot 会把它当作一个容器过滤器自动注册到 Tomcat 的 FilterChain 里。这不是不对但它不会出现在 FilterChainProxy 内部的 ListFilter 中也就是说它不会按照 Spring Security 的规则参与安全过滤。如果你希望在安全过滤器链中插入自定义逻辑应该在 HttpSecurity 配置里显式指定http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);不加的话你可能会看到“过滤器明明执行了但执行时机不是自己想要的”这种情况尤其是在权限判断之前还是之后执行差别非常明显。坑 2多个 SecurityFilterChain 时顺序不对。第一条链没有配置securityMatcher导致所有请求都被它匹配到后面的链完全闲置。排查方法很简单看 FilterChainProxy 里 FilterChains 的遍历结果确认每个请求实际命中了哪条链。坑 3还在使用 antMatchers 等旧 API。Spring Security 6 已经移除了antMatchers、mvcMatchers、regexMatchers统一使用requestMatchers。这不仅是名字变化匹配语义也有差异。遇到“URL 明明是对的但一直 403”的情况先去检查是不是还在使用旧 API或者使用了字符串但没加requestMatchers前缀。最后再分享一个我自己的排错习惯只要遇到认证授权相关的问题我从不去猜配置而是先确定两件事——这条请求匹配到了哪条 SecurityFilterChain以及它停在了哪个过滤器的哪个方法上。只要把 FilterChainProxy 的匹配过程和 VirtualFilterChain 的执行顺序都定位清楚大部分问题都能在半小时内找到根因。也正因如此我建议每个用 Spring Security 的人都花点时间把这条注入链跑通一遍不要只停留在会用配置的层面。
返回列表