Spring Security 的过滤器链,跟你的责任链差了一个生产环境的距离
责任链模式是面试八股里的常客。标准答案:定义一个 Handler 接口,每个 Handler 持有下一个 Handler 的引用,处理完往后传。然后写个例子——请假审批:组长批三天,经理批七天,超过七天找总监。
这的确能跑。但如果你写过 Spring Security 的配置,你会发现它用的过滤器链跟这个"标准责任链"差别大到你怀疑自己学了个假模式。
标准的责任链长什么样
先回忆一下教科书版本:
```java public abstract class Approver { protected Approver next;
public void setNext(Approver next) { this.next = next; } public abstract void handle(Request request);}
public class Manager extends Approver { @Override public void handle(Request request) { if (request.getDays() <= 7) { System.out.println("经理审批通过"); } else if (next != null) { next.handle(request); } } } ```
这个设计有两个硬伤。
第一,链是静态的。你必须在代码里把每个节点的next设好,运行时改不了顺序。春节来了想说"总监审批下放到经理"?对不起,代码要改。
第二,节点自己决定要不要往后传。Manager里写了next.handle(request),如果某个实现忘了调 next,整条链就断了。责任链的完整性依赖每个节点的"自觉",这在多人协作的项目里是定时炸弹。
Spring Security 的 VirtualFilterChain:谁才是链的主人
Spring Security 的过滤器链不是节点串联——是一个独立的链对象控制所有节点的执行顺序。
```java // SecurityFilterChain 的实现 public final class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; private final List
public VirtualFilterChain(FilterChain originalChain, List<Filter> additionalFilters) { this.originalChain = originalChain; this.additionalFilters = additionalFilters; this.size = additionalFilters.size(); } @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (currentPosition == size) { // 所有 Security Filter 执行完了,交给原始链(最终到达 Servlet) originalChain.doFilter(request, response); return; } currentPosition++; Filter nextFilter = additionalFilters.get(currentPosition - 1); nextFilter.doFilter(request, response, this); }} ```
跟标准责任链的区别:
| | 标准责任链 | Spring Security VirtualFilterChain | |---|---|---| | 链的控制权 | 分散在每个节点 | 集中在 VirtualFilterChain | | 链的顺序 | 代码硬编码 | 通过配置(SecurityConfig)动态决定 | | 节点的职责 | 自己决定要不要传 | 只管自己的逻辑,传不传由链控制 | | 跳过节点 | 不支持 | 通过 RequestMatcher 控制哪些 URL 走哪些过滤器 | | 链的创建 | 代码里 new 出来 | IoC 容器管理,声明式配置 |
VirtualFilterChain 用一个计数器currentPosition迭代 List,每个 Filter 收到this(VirtualFilterChain 本身)作为FilterChain参数。Filter 处理完自己的逻辑后调用chain.doFilter()——此时 VirtualFilterChain 的doFilter又执行一次,currentPosition++,取下一个 Filter。
控制权反转了。Filter 不持有下一个 Filter 的引用,VirtualFilterChain 持有所有 Filter 并决定执行顺序。Filter 只管"我要做什么",VirtualFilterChain 管"按什么顺序做"。
你以为只是顺序问题?还支持跳过
Spring Security 的过滤器链还有一个标准责任链做不到的事:针对不同 URL 使用不同的过滤器组合。
java @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); }
/api/public/**不需要走UsernamePasswordAuthenticationFilter。Spring Security 怎么做到的?不是写个 if 判断在 Filter 里跳过——是RequestMatcher在链的构建阶段就决定了哪些 URL 走哪些 Filter。
这意味着过滤器链在创建的时候就"瘦身"了,不是运行时在每个 Filter 里检查是否需要跳过。对于高并发场景,这省了大量无意义的判断开销。
SecurityContextHolder:ThreadLocal 模式的精准应用
过滤器链之外,Spring Security 还藏了一个经典模式——SecurityContextHolder用 ThreadLocal 存储当前请求的安全上下文:
```java public class SecurityContextHolder { private static final ThreadLocal contextHolder = new ThreadLocal<>();
public static SecurityContext getContext() { SecurityContext ctx = contextHolder.get(); if (ctx == null) { ctx = createEmptyContext(); contextHolder.set(ctx); } return ctx; }} ```
在一个请求的生命周期里,Filter 链校验通过后把认证信息设进去,Controller 层直接拿:
java Authentication auth = SecurityContextHolder.getContext() .getAuthentication();
不需要把Authentication对象作为参数一路从 Filter 传到 Controller 再到 Service——ThreadLocal 让它在当前线程的任何地方都能拿到。这就是 ThreadLocal 模式的精髓:避免参数透传污染方法签名。
但 ThreadLocal 有个坑:线程池复用场景下,上一个请求的 ThreadLocal 可能残留到下一个请求。Spring Security 的SecurityContextPersistenceFilter在请求结束后显式清理:
java finally { SecurityContextHolder.clearContext(); }
不清理会怎样?用户 A 登出后,同一个线程被分配给用户 B 的请求,用户 B 拿到了用户 A 的认证信息——严重的安全漏洞。这就是为什么模式要用对地方:ThreadLocal 能解决参数透传,但你必须管好生命周期。
动手写一个支持动态编排的责任链
理解了 VirtualFilterChain 的思路,你自己也能写一个支持动态编排的责任链。假设你要做一个请求限流框架:
```java public interface RateLimitHandler { boolean handle(Request request); }
public class RateLimitChain { private final List handlers;
public RateLimitChain(List<RateLimitHandler> handlers) { this.handlers = handlers; } public boolean execute(Request request) { for (RateLimitHandler handler : handlers) { if (!handler.handle(request)) { return false; // 任意一个拦截就拒绝 } } return true; // 全部通过 }} ```
调用方通过构造参数注入处理器列表,顺序由列表决定。需要调顺序?改列表顺序,不改代码。需要加一个处理器?add 到列表里。
相比于传统的 Handler 链式持有 next 引用,这种"集中调度"的方式让链的构建逻辑和控制逻辑完全分离。链的构建是配置层的事(YAML、注解、代码配置),链的执行是运行时的事——两者不再耦合。
回到你的代码
说实话,大部分项目里你用不到 Spring Security 级别的过滤器链设计。但有一条是普适的:不要让链上的节点自己决定要不要往后传。把"链的控制权"从节点手里收回来,交给一个中心化的链对象。
这跟用不用 Spring Security 无关——哪怕你现在写的只是审批流、数据校验管道、消息拦截器链,这条原则也适用。
写设计模式的代码不难,难的是看到一段代码时认出"它在实现什么模式"以及"它在什么地方和标准模式不一样"。Spring Security 的过滤器链就是一个典型的变形——骨架是责任链,但控制权集中化、支持动态编排、与 IoC 容器深度集成。你读懂了这种变形,才算真正理解了责任链。
做了个用卡皮巴拉漫画讲设计模式的小程序「爪爪代码冒险记」。Spring Security 这些"标准模式的变形"在里面用关卡的方式呈现——先让你写标准责任链,再改造出 VirtualFilterChain 的效果。如果你觉得这种从源码反推模式的思路有用,搜一下试试。