SpringSecurity源码解析三:FilterChainProxy核心代理:智能路由、防火墙与请求分发

在 Spring Security 的架构中,FilterChainProxy 是整个安全过滤器链的"总指挥"和"总入口"。它本身并不直接处理具体的认证或授权逻辑,而是负责请求的防火墙校验、智能路由分发、生命周期管理以及异常兜底 。本文将结合 Spring Security 6.5.x 正式源码,深度拆解 FilterChainProxy 及其构建者 WebSecurity 的核心运行机制。


前言

在前两篇文章中,我们理清了过滤器的执行顺序(FilterOrderRegistration)和构建过程(HttpSecurity → DefaultSecurityFilterChain)。但一个关键问题还没回答:

构建好的过滤器链,是如何被"激活"的?每个 HTTP 请求进来后,到底是谁在调度这些过滤器? 答案就是 FilterChainProxy------Spring Security 的"总代理"。


一、FilterChainProxy:Spring Security 的总入口

在标准的 Spring Boot 应用中,Servlet 容器(如 Tomcat)并不直接感知 Spring Security 内部庞大的过滤器链。它们之间通过 DelegatingFilterProxy 进行桥接:

  1. Servlet 容器 只知道一个名为 springSecurityFilterChain 的标准 Servlet Filter。
  2. DelegatingFilterProxy 拦截请求,并将其委托给 Spring IoC 容器中真正的 Bean ------ FilterChainProxy
  3. FilterChainProxy 内部维护了一个 List<SecurityFilterChain>,根据请求的 URL 短路匹配(short-circuit matching) 智能路由到具体的过滤器链执行:遍历所有 SecurityFilterChain,第一个匹配的生效,后续的忽略

说明SecurityFilterChain 接口只有两个方法:matches(HttpServletRequest)getFilters()matches() 用于判断当前请求是否属于该链的管辖范围,getFilters() 返回该链所包含的 Spring Security 过滤器列表。由于采用短路匹配机制,必须将最具体的匹配规则放在最前面 (例如 /admin/** 应排在 /** 之前)。


二、WebSecurity:FilterChainProxy 的构建者

FilterChainProxy 并不是凭空产生的,它是由 WebSecurity 在 Spring 容器初始化时构建出来的。WebSecurity 继承了 AbstractConfiguredSecurityBuilder<Filter, WebSecurity> 并实现了 SecurityBuilder<Filter> 接口,其核心构建逻辑在 performBuild() 方法中。

2.1 核心职责

  • 收集并构建所有的 SecurityFilterChain(由 HttpSecurity 构建出来的每条安全过滤器链)。
  • 反向提取 所有链中的授权规则,构建全局的授权评估器(WebInvocationPrivilegeEvaluator)。
  • 组装 FilterChainProxy 并注入防火墙(HttpFirewall)和拒绝处理器(RequestRejectedHandler)。
  • 自动感知 Spring 容器中的扩展组件(ObservationRegistryHttpFirewall Bean、FilterChainDecorator 后处理器等)。

2.2 容器初始化阶段的自动装配 (WebSecurity.setApplicationContext)

performBuild() 真正执行之前,WebSecurity 通过 setApplicationContext() 从 Spring IoC 容器中自动感知 以下可选组件(源码位于 WebSecurity.java 第 240-265 行附近):

自动装配项 说明
HttpFirewall Bean 若容器中存在用户自定义的 HttpFirewall Bean,则注入;否则为空(留待 FilterChainProxy 使用自身默认的 StrictHttpFirewall
RequestRejectedHandler Bean 若容器中存在用户自定义的拒绝处理器,则注入
ObservationRegistry Bean 若引入 Micrometer,则自动获取观察注册表;否则使用 ObservationRegistry.NOOP
ObjectPostProcessor<FilterChainDecorator> 用于后处理 VirtualFilterChainDecorator,Observability 模块正是通过此机制注入 ObservationFilterChainDecorator
HttpServletRequestTransformer Bean 用于在 PrivilegeEvaluator 评估前对请求进行转换(如包装请求)

2.3 实际构建阶段 (WebSecurity.performBuild)

当 Spring 容器初始化 springSecurityFilterChain Bean 时,会触发 webSecurity.build(),最终调用 WebSecurity.performBuild()

java 复制代码
@Override
protected Filter performBuild() {
    Assert.state(!this.securityFilterChainBuilders.isEmpty(),
          () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
                + "Typically this is done by exposing a SecurityFilterChain bean. "
                + "More advanced users can invoke " + WebSecurity.class.getSimpleName()
                + ".addSecurityFilterChainBuilder directly");

    int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
    List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
    
    //  构建授权管理器 Builder,用于后续反向提取所有链的授权规则
    RequestMatcherDelegatingAuthorizationManager.Builder builder = 
          RequestMatcherDelegatingAuthorizationManager.builder();
    boolean mappings = false;

    // ① 处理 ignored requests(官方强烈不推荐)
    for (RequestMatcher ignoredRequest : this.ignoredRequests) {
       this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest
             + ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.");
       // 创建"空过滤器链":仅含 RequestMatcher,filters 列表为空
       SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
       securityFilterChains.add(securityFilterChain);
       // 将 ignored 规则同步加入授权管理器(标记为 permitAll)
       builder.add(ignoredRequest, SingleResultAuthorizationManager.permitAll());
       mappings = true;
    }

    // ② 构建所有用户定义的 SecurityFilterChain
    for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
       SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
       securityFilterChains.add(securityFilterChain);
       // 提取链中的授权规则,同步加入全局授权管理器
       // 兼容两种授权过滤器:AuthorizationFilter(6.x 推荐)和 FilterSecurityInterceptor(5.x 遗留)
       mappings = addAuthorizationManager(securityFilterChain, builder) || mappings;
    }

    // ③ 构建 WebInvocationPrivilegeEvaluator(专门用于 JSP <sec:authorize> 等视图层标签)
    if (this.privilegeEvaluator == null) {
       AuthorizationManager<HttpServletRequest> authorizationManager = mappings ? builder.build()
             : SingleResultAuthorizationManager.permitAll();
       AuthorizationManagerWebInvocationPrivilegeEvaluator privilegeEvaluator = 
             new AuthorizationManagerWebInvocationPrivilegeEvaluator(authorizationManager);
       privilegeEvaluator.setServletContext(this.servletContext);
       if (this.privilegeEvaluatorRequestTransformer != null) {
          privilegeEvaluator.setRequestTransformer(this.privilegeEvaluatorRequestTransformer);
       }
       this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
             List.of(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, List.of(privilegeEvaluator))));
    }

    // ④ 创建 FilterChainProxy 并注入所有链
    FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);

    // ⑤ 配置防火墙
    // 注意:如果用户在容器中定义了 HttpFirewall Bean,则优先使用;
    // 否则 FilterChainProxy 内部默认使用 StrictHttpFirewall(通过成员变量初始化)
    if (this.httpFirewall != null) {
       filterChainProxy.setFirewall(this.httpFirewall);
    }

    // ⑥ 配置请求拒绝处理器(支持 Micrometer 可观测性)
    if (this.requestRejectedHandler != null) {
       filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
    }
    else if (!this.observationRegistry.isNoop()) {
       // 如果引入了 Micrometer,自动包装观测处理 + HTTP 400 响应的组合处理器
       CompositeRequestRejectedHandler requestRejectedHandler = new CompositeRequestRejectedHandler(
             new ObservationMarkingRequestRejectedHandler(this.observationRegistry),
             new HttpStatusRequestRejectedHandler());
       filterChainProxy.setRequestRejectedHandler(requestRejectedHandler);
    }
    // 如果以上条件都不满足,FilterChainProxy 使用自身默认的 HttpStatusRequestRejectedHandler

    // ⑦ 验证、装饰与初始化
    filterChainProxy.setFilterChainValidator(new WebSecurityFilterChainValidator());
    filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());
    filterChainProxy.afterPropertiesSet();

    // ⑧ 如果开启 debug,包装 DebugFilter 并打印安全警告
    Filter result = filterChainProxy;
    if (this.debugEnabled) {
       this.logger.warn("\n\n" + "********************************************************************\n"
             + "**********        Security debugging is enabled.       *************\n"
             + "**********    This may include sensitive information.  *************\n"
             + "**********      Do not use in a production system!     *************\n"
             + "********************************************************************\n\n");
       result = new DebugFilter(filterChainProxy);
    }

    this.postBuildAction.run();
    return result;
}

构建步骤深度总结(6.5.x 源码级)

步骤 操作 6.5.x 源码深度解析
处理 ignored requests 创建空过滤器链DefaultSecurityFilterChain 的 filters 列表为空),并打印建议废弃的警告日志。在 FilterChainProxy 匹配到该链时,会走 "no security" 路径。
构建 SecurityFilterChain 调用每个 HttpSecuritybuild(),并提取其中的授权规则。兼容 AuthorizationFilter(6.x 推荐)和 FilterSecurityInterceptor(5.x 遗留)两种授权过滤器
构建 PrivilegeEvaluator 6.x 核心机制 :将上述收集到的所有路由和授权规则,反向构建成一个全局 AuthorizationManager,用于支撑 JSP <sec:authorize url="/admin">视图层 URL 鉴权标签。
创建 FilterChainProxy 注入所有构建好的 SecurityFilterChainFilterChainProxy 先于 WebSecurity 设置了默认值:StrictHttpFirewallHttpStatusRequestRejectedHandlerVirtualFilterChainDecorator
配置 HttpFirewall 若用户定义了 HttpFirewall Bean 则注入;否则 FilterChainProxy 保留自身默认的 StrictHttpFirewall
配置 RequestRejectedHandler 若用户定义了 Handler Bean 则注入;若引入 Micrometer 则自动装配组合处理器;否则保留 FilterChainProxy 默认的 HttpStatusRequestRejectedHandler

WebInvocationPrivilegeEvaluator 仅用于视图层 URL 鉴权 (如 JSP <sec:authorize url="/admin"> 标签),不涉及方法级安全

@PreAuthorize@PostAuthorize 等方法级注解由完全独立的方法安全拦截器体系处理:

  • 6.x 推荐:AuthorizationManagerBeforeMethodInterceptor / AuthorizationManagerAfterMethodInterceptor
  • 5.x 遗留:MethodSecurityInterceptor

WebSecurity.performBuild() 之所以要反向遍历所有 SecurityFilterChain 提取授权规则,是为了确保 <sec:authorize> 标签中的 URL 判断结果与过滤器链的实际授权行为完全一致------这是 Spring Security 6.x 自动集成视图层鉴权的底层原理。

2.4 addAuthorizationManager:反向提取授权规则的实现

这是 performBuild() 中最关键的辅助方法,体现了 6.x 对两种授权过滤器的兼容设计:

java 复制代码
private boolean addAuthorizationManager(SecurityFilterChain securityFilterChain,
        RequestMatcherDelegatingAuthorizationManager.Builder builder) {
    boolean mappings = false;
    for (Filter filter : securityFilterChain.getFilters()) {
        // 兼容 5.x 遗留的 FilterSecurityInterceptor
        if (filter instanceof FilterSecurityInterceptor securityInterceptor) {
            DefaultWebInvocationPrivilegeEvaluator privilegeEvaluator = 
                new DefaultWebInvocationPrivilegeEvaluator(securityInterceptor);
            privilegeEvaluator.setServletContext(this.servletContext);
            AuthorizationManager<RequestAuthorizationContext> authorizationManager = 
                (authentication, context) -> {
                    HttpServletRequest request = context.getRequest();
                    boolean result = privilegeEvaluator.isAllowed(
                        request.getContextPath(), request.getRequestURI(),
                        request.getMethod(), authentication.get());
                    return new AuthorizationDecision(result);
                };
            builder.add(securityFilterChain::matches, authorizationManager);
            mappings = true;
            continue;
        }
        // 6.x 推荐的 AuthorizationFilter
        if (filter instanceof AuthorizationFilter authorization) {
            AuthorizationManager<HttpServletRequest> authorizationManager = 
                authorization.getAuthorizationManager();
            // 将 HttpServletRequest 类型的授权管理器适配为 RequestAuthorizationContext 类型
            builder.add(securityFilterChain::matches,
                (authentication, context) -> (AuthorizationDecision) authorizationManager
                    .authorize(authentication, context.getRequest()));
            mappings = true;
        }
    }
    return mappings;
}

设计要点RequestMatcherDelegatingAuthorizationManager.Builder 要求 AuthorizationManager<RequestAuthorizationContext> 类型,而 AuthorizationFilter 实际持有的是 AuthorizationManager<HttpServletRequest>(通过 AuthorizeHttpRequestsConfigurer 配置)。此处通过 Lambda 适配器完成类型转换,是典型的适配器模式应用。


三、FilterChainProxy.doFilter() 源码拆解

这是所有 HTTP 请求进入 Spring Security 的第一道门槛。在 6.x 版本中,FilterChainProxy 将逻辑拆分为了外层的 doFilter(负责生命周期与异常兜底)和内层的 doFilterInternal(负责防火墙与路由驱动)。

3.1 doFilter 主流程:防重入、异常兜底与上下文清理

java 复制代码
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    
    // ① 防重入机制:检查 FILTER_APPLIED 标记
    // 使用 FilterChainProxy 类全限定名 + ".APPLIED" 作为 attribute key
    boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
    if (!clearContext) {
        // 如果已标记(如 forward/include 场景),直接走内部逻辑,不再清理上下文
        doFilterInternal(request, response, chain);
        return;
    }
    
    try {
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        // ② 执行真正的安全过滤逻辑
        doFilterInternal(request, response, chain);
    }
    catch (Exception ex) {
        // ③ 使用 ThrowableAnalyzer 解包异常链,定位 RequestRejectedException
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
        Throwable requestRejectedException = this.throwableAnalyzer
            .getFirstThrowableOfType(RequestRejectedException.class, causeChain);
            
        if (!(requestRejectedException instanceof RequestRejectedException)) {
            throw ex; // 不是防火墙拒绝异常,继续向上抛出(由 Servlet 容器处理)
        }
        // 交给 RequestRejectedHandler 处理(默认 HttpStatusRequestRejectedHandler → HTTP 400)
        this.requestRejectedHandler.handle((HttpServletRequest) request, 
                (HttpServletResponse) response, (RequestRejectedException) requestRejectedException);
    }
    finally {
        // ④ 核心兜底:清理 SecurityContext,防止线程池复用导致信息串号
        this.securityContextHolderStrategy.clearContext();
        // 移除防重入标记
        request.removeAttribute(FILTER_APPLIED);
    }
}

补充说明ThrowableAnalyzer 通过反射遍历异常链(getCause()),可以穿透 ServletExceptionInvocationTargetException 等包装异常,精准定位到真正的 RequestRejectedException

3.2 doFilterInternal:防火墙校验与路由驱动(双路径分支)

java 复制代码
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
        
    // ① 防火墙校验:同时包装请求和响应
    FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
    
    // ② 路由匹配:根据请求 URL 短路匹配对应的过滤器链
    List<Filter> filters = getFilters(firewallRequest);
    
    // ───── 分支 A:没有匹配的链 ─────
    if (filters == null || filters.isEmpty()) {
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
        }
        // 不做任何安全处理,重置防火墙包装,直接调用原始 Servlet FilterChain
        firewallRequest.reset();
        this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
        return;
    }
    
    // ───── 分支 B:匹配到了安全过滤器链 ─────
    if (logger.isDebugEnabled()) {
        logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
    }
    
    // ③ 构造 reset Lambda:当所有安全过滤器执行完毕后触发
    FilterChain reset = (req, res) -> {
        if (logger.isDebugEnabled()) {
            logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
        }
        firewallRequest.reset();     // 恢复原始的 servletPath 和 pathInfo
        chain.doFilter(req, res);    // 交还给 Servlet 容器的原始 FilterChain
    };
    
    // ④ 装饰并驱动虚拟过滤器链执行
    this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
}

分支 A(无匹配链) 的场景包括:

  • ignored requests(空过滤器链的 DefaultSecurityFilterChain
  • 请求 URL 未命中任何 SecurityFilterChain 的 matches()

分支 B(有匹配链) 是正常的安全处理流程。

核心设计解析

  1. FilterChain reset 的设计 :源码中这个 reset 是一个 Lambda 表达式实现的 FilterChain。作为 VirtualFilterChainoriginalChain(原始链)。当 VirtualFilterChain 中所有安全过滤器递归执行完毕,调用 originalChain.doFilter() 时,实际执行的是这个 Lambda------先重置防火墙请求(恢复原始路径信息),再调用 Servlet 容器的原始 FilterChain

  2. FilterChainDecorator 装饰器模式 :6.x 引入的 FilterChainDecorator 接口由 VirtualFilterChainDecorator 默认实现。如果引入了 Spring Security Observability(Micrometer),Spring 容器中的 ObjectPostProcessor<FilterChainDecorator> 会将其替换为带有追踪逻辑的 ObservationFilterChainDecorator,从而对每个过滤器的执行进行可观测性埋点。

3.3 getFilters:短路匹配机制

java 复制代码
// 路由匹配:短路机制,第一个匹配的 SecurityFilterChain 生效
private List<Filter> getFilters(HttpServletRequest request) {
    int count = 0;
    for (SecurityFilterChain chain : this.filterChains) {
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", 
                chain, ++count, this.filterChains.size()));
        }
        if (chain.matches(request)) {
            return chain.getFilters();
        }
    }
    return null;
}

关键设计getFilters() 返回的是 List<Filter>(过滤器列表),不是 SecurityFilterChain 本身。这是短路匹配的体现:一旦找到第一个匹配的链,立即返回其过滤器列表,不再检查后续的链。因此链的顺序至关重要

3.4 VirtualFilterChain:递归驱动的虚拟过滤器链

java 复制代码
// FilterChainProxy 的内部私有类
private static final class VirtualFilterChain implements FilterChain {
    private final FilterChain originalChain;      // 原始链(即 reset Lambda)
    private final List<Filter> additionalFilters; // Spring Security 过滤器列表
    private final int size;
    private int currentPosition = 0;              // 当前位置指针

    private VirtualFilterChain(FilterChain chain, List<Filter> additionalFilters) {
        this.originalChain = chain;
        this.additionalFilters = additionalFilters;
        this.size = additionalFilters.size();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response) 
            throws IOException, ServletException {
        if (this.currentPosition == this.size) {
            // 所有安全过滤器执行完毕 → 调用 originalChain
            // (触发 firewallRequest.reset() → 调用 Servlet 原始 FilterChain)
            this.originalChain.doFilter(request, response);
            return;
        }
        this.currentPosition++;
        Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
        if (logger.isTraceEnabled()) {
            String name = nextFilter.getClass().getSimpleName();
            logger.trace(LogMessage.format("Invoking %s (%d/%d)", 
                name, this.currentPosition, this.size));
        }
        // 递归调用:将自身作为 FilterChain 传给下一个过滤器
        nextFilter.doFilter(request, response, this);
    }
}

递归驱动原理VirtualFilterChain 通过 currentPosition 计数器实现递归式过滤器链驱动。每个过滤器在完成自身逻辑后调用 chain.doFilter(),实际是回调 VirtualFilterChain.doFilter(),使得 currentPosition 递增并驱动下一个过滤器。这完全符合 Servlet Filter 的责任链模式规范。

3.5 FilterChainDecorator 接口与扩展

java 复制代码
// FilterChainProxy 内部定义的公开接口
public interface FilterChainDecorator {
    // 无安全过滤器时的装饰
    default FilterChain decorate(FilterChain original) {
        return decorate(original, Collections.emptyList());
    }
    
    // 有安全过滤器时的装饰
    FilterChain decorate(FilterChain original, List<Filter> filters);
}

// 默认实现
public static final class VirtualFilterChainDecorator implements FilterChainDecorator {
    @Override
    public FilterChain decorate(FilterChain original) {
        return original;  // 无过滤器时,直接返回原始链
    }
    
    @Override
    public FilterChain decorate(FilterChain original, List<Filter> filters) {
        return new VirtualFilterChain(original, filters);  // 包装为 VirtualFilterChain
    }
}

FilterChainDecorator 接口和 FilterChainDecorator 后处理器机制自 6.0 引入,为 Observability 等横切关注点提供了标准的扩展点。


四、HttpFirewall:请求防火墙详解

Spring Security 提供了 HttpFirewall 接口用于在请求进入过滤器链之前进行拦截和包装。它有两个核心接口方法:

java 复制代码
public interface HttpFirewall {
    FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException;
    HttpServletResponse  getFirewalledResponse(HttpServletResponse response);
}

两个核心实现类:StrictHttpFirewall(6.x 当前默认)和 DefaultHttpFirewall(旧版默认)。

4.1 响应防火墙:FirewalledResponse

两个防火墙实现都通过 getFirewalledResponse() 返回 FirewalledResponse(源码位于 web/src/main/java/.../firewall/FirewalledResponse.java),它负责:

  • CRLF 注入防护 :拦截 sendRedirect()setHeader()addHeader()addCookie(),检查参数中是否包含 \r(回车)或 \n(换行)字符
  • Cookie 安全校验 :对 Cookie 的 namevaluepathdomaincomment 全部进行 CRLF 检测

4.2 默认实现:StrictHttpFirewall(严格模式)

StrictHttpFirewall 是 Spring Security 6.x 的默认防火墙实现 。它的设计哲学是:"与其尝试去清洗(sanitize)一个恶意的 URL,不如直接拒绝它"

java 复制代码
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
    // ① 拒绝非法的 HTTP 方法(默认只允许 GET/HEAD/POST/PUT/PATCH/DELETE/OPTIONS)
    rejectForbiddenHttpMethod(request);
    
    // ② 拒绝包含黑名单字符的 URL
    // 分两次校验:先查 encodedUrlBlocklist(编码后的 URL),再查 decodedUrlBlocklist(解码后的 URL)
    rejectedBlocklistedUrls(request);
    
    // ③ 拒绝不受信任的 Host 主机名
    rejectedUntrustedHosts(request);
    
    // ④ 拒绝未规范化的 URL(防目录遍历攻击,如包含 ./ 或 ../)
    if (!isNormalized(request)) {
        throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
    }
    
    // ⑤ 拒绝 requestURI 中包含非可打印 ASCII 字符的请求(范围 0x20 ~ 0x7e 之外)
    rejectNonPrintableAsciiCharactersInFieldName(request.getRequestURI(), "requestURI");
    
    // ⑥ ★ 返回 StrictFirewalledRequest,将 Header/Parameter 校验延迟到实际调用 getter 时
    return new StrictFirewalledRequest(request);
}

4.2.1 StrictFirewalledRequest:延迟校验的设计

StrictFirewalledRequestStrictHttpFirewall 的内部类,继承自 FirewalledRequest重写了所有 Header 和 Parameter 的 getter 方法,在返回数据之前进行安全校验:

scss 复制代码
方法级别校验:
  - getHeader(name)          → validateAllowedHeaderName(name) + validateAllowedHeaderValue(name, value)
  - getHeaders(name)         → validateAllowedHeaderName(name) + validateAllowedHeaderValue(逐个值)
  - getHeaderNames()         → validateAllowedHeaderName(逐个 Header 名)
  - getDateHeader(name)      → validateAllowedHeaderName(name)
  - getIntHeader(name)       → validateAllowedHeaderName(name)
  - getParameter(name)       → validateAllowedParameterName(name) + validateAllowedParameterValue(name, value)
  - getParameterValues(name) → validateAllowedParameterName(name) + validateAllowedParameterValue(逐个值)
  - getParameterNames()      → validateAllowedParameterName(逐个参数名)
  - getParameterMap()        → validateAllowedParameterName + validateAllowedParameterValue(全量遍历)

默认的校验策略:

  • Header/Parameter Name :只允许可打印且非 ISO 控制字符(正则 \p{IsAssigned} && [^\p{IsControl}]
  • Header Value :允许可打印字符 + Tab(正则 \p{IsAssigned} && [[^\p{IsControl}] || \t]
  • Parameter Value :默认不限制(value -> true

4.2.2 StrictHttpFirewall 的可配置开关

配置方法 默认值 说明
setAllowedHttpMethods(Collection) 7种标准法 限制允许的 HTTP 方法
setUnsafeAllowAnyHttpMethod(boolean) false 设为 true 则不对 HTTP 方法做任何校验(不推荐)
setAllowSemicolon(boolean) false 是否允许 URL 中的分号 ;(矩阵变量)
setAllowUrlEncodedSlash(boolean) false 是否允许 URL 编码的斜杠 %2F / %2f
setAllowUrlEncodedDoubleSlash(boolean) false 是否允许 URL 编码的双斜杠 %2F%2F 等变体
setAllowUrlEncodedPeriod(boolean) false 是否允许 URL 编码的句号 %2E / %2e
setAllowBackSlash(boolean) false 是否允许反斜杠 \\%5C / %5c
setAllowNull(boolean) false 是否允许空字节 \0%00
setAllowUrlEncodedPercent(boolean) false 是否允许 URL 编码的百分号 %25(防双重编码)
setAllowUrlEncodedCarriageReturn(boolean) false 是否允许 URL 编码的回车 %0d / %0D
setAllowUrlEncodedLineFeed(boolean) false 是否允许 URL 编码的换行 %0a / %0A
setAllowUrlEncodedLineSeparator(boolean) false 是否允许行分隔符
setAllowUrlEncodedParagraphSeparator(boolean) false 是否允许段分隔符
setAllowedHostnames(Predicate) true 主机名校验(默认全部允许)
setAllowedHeaderNames(Predicate) 非控制字符 自定义 Header 名称校验
setAllowedHeaderValues(Predicate) 非控制字符 自定义 Header 值校验
setAllowedParameterNames(Predicate) 非控制字符 自定义参数名校验
setAllowedParameterValues(Predicate) true 自定义参数值校验

4.3 旧版实现:DefaultHttpFirewall(宽松/清洗模式)

DefaultHttpFirewall 是早期的默认实现。它的设计哲学是:"尝试清洗和规范化请求,而不是直接拒绝"

java 复制代码
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
    // ① ★ 包装请求为 RequestWrapper
    // RequestWrapper 在构造时自动剥离路径中的分号(矩阵变量)和连续双斜杠
    FirewalledRequest firewalledRequest = new RequestWrapper(request);
    
    // ② 仅校验清洗后的 servletPath 和 pathInfo 是否规范化(防目录遍历)
    if (!isNormalized(firewalledRequest.getServletPath()) || !isNormalized(firewalledRequest.getPathInfo())) {
        throw new RequestRejectedException("Un-normalized paths are not supported: ...");
    }
    
    // ③ 仅校验 requestURI 是否包含编码的斜杠(%2f / %2F)
    // 可通过 setAllowUrlEncodedSlash(true) 放开
    String requestURI = firewalledRequest.getRequestURI();
    if (containsInvalidUrlEncodedSlash(requestURI)) {
        throw new RequestRejectedException("The requestURI cannot contain encoded slash. Got " + requestURI);
    }
    
    return firewalledRequest;
}

关键区别DefaultHttpFirewallRequestWrapper主动修改 servletPathpathInfo(剥离分号后的路径参数和连续双斜杠)。而 StrictHttpFirewallStrictFirewalledRequest 不修改路径值,只是拒绝不合规的请求。这体现了两种完全不同的安全哲学。

4.4 两者对比与设计演进

对比维度 DefaultHttpFirewall (旧版) StrictHttpFirewall (6.x 默认)
HTTP 方法校验 ❌ 不校验 ✅ 默认仅允许 7 种标准方法
URL 分号处理 ✅ 主动剥离分号及其后内容 ❌ 直接拒绝包含分号的 URL
URL 黑名单字符 ❌ 仅校验编码斜杠和目录遍历 ✅ 维护庞大的双重黑名单(编码+解码)
Header/Param 校验 ❌ 不校验 ✅ 延迟校验,拦截控制字符(防 CRLF 注入)
Host 校验 ❌ 不校验 ✅ 支持 Predicate 自定义主机名白名单
双斜杠处理 ✅ 主动合并连续斜杠 ❌ 直接拒绝 //
安全性 较低,容易被编码绕过 极高,多层纵深防御

4.5 FirewalledRequest.reset() 的设计

FirewalledRequest 是一个抽象类,定义了 abstract void reset() 方法:

  • StrictFirewalledRequest.reset() :空实现。因为 StrictHttpFirewall 不修改请求路径,无需恢复。
  • RequestWrapper.reset()DefaultHttpFirewall 使用):通过 stripPaths = false 标志位恢复原始的未经剥离的 servletPathpathInfo

FilterChainProxy 在安全过滤完成之后调用 firewallRequest.reset(),确保回到 Servlet 容器原始链时,请求对象的路径信息已恢复到防火墙包装前的状态。


五、SecurityContext 的生命周期管理(双重清理机制)

Spring Security 采用了**"双重清理(Double Clear)"兜底机制**,配合两套独立的 FILTER_APPLIED 防重入标记,以确保在 Tomcat 线程池复用场景下,绝对不会发生上下文串号泄漏。

5.1 两套独立的 FILTER_APPLIED 防重入标记

组件 FILTER_APPLIED Key 作用
FilterChainProxy FilterChainProxy.class.getName() + ".APPLIED" 防止 FilterChainProxy 被重复执行
SecurityContextHolderFilter SecurityContextHolderFilter.class.getName() + ".APPLIED" 防止 SecurityContext 被重复加载/清理

两套标记完全独立,这是分层防御理念在请求属性层面的体现。

5.2 第一重清理:SecurityContextHolderFilter(链内常规清理)

在 6.x 中,SecurityContextHolderFilter 负责在请求进入时延迟加载 上下文,并在其 finally 块中进行常规清理:

java 复制代码
// SecurityContextHolderFilter.doFilter(实际源码)
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws ServletException, IOException {
    // 独立的 FILTER_APPLIED 防重入检查
    if (request.getAttribute(FILTER_APPLIED) != null) {
        chain.doFilter(request, response);
        return;
    }
    request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
    
    // 延迟加载:loadDeferredContext 返回 Supplier<SecurityContext>
    // SecurityContext 只有在首次调用 supplier.get() 时才真正从 Repository 加载
    Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
    
    try {
        // 将延迟加载的上下文设置到 SecurityContextHolder
        this.securityContextHolderStrategy.setDeferredContext(deferredContext);
        chain.doFilter(request, response);
    }
    finally {
        // 第一重清理:正常流程下的上下文回收
        this.securityContextHolderStrategy.clearContext();
        request.removeAttribute(FILTER_APPLIED);
    }
}

💡 延迟加载设计(Supplier<SecurityContext>)的意义

在 6.x 中,SecurityContextRepository.loadDeferredContext() 返回的是 Supplier<SecurityContext> 而非直接的 SecurityContext。这意味着:

  • 如果整个请求处理过程中没有任何代码访问 SecurityContextHolder.getContext() (例如访问的是一个完全公开的资源),那么 SecurityContext永远不会被加载
  • 避免了不必要的 Session 读取、反序列化等昂贵操作,极大提升了公开资源的吞吐量。
  • 配合 SecurityContextHolderStrategy.setDeferredContext(),只有在首次 getContext() 调用时才触发 supplier.get()

5.3 第二重清理:FilterChainProxy(外层绝对兜底)

正如我们在 FilterChainProxy 源码中看到的,作为整个安全体系的入口,它在最外层的 finally 块中,再次执行了清理操作

java 复制代码
// FilterChainProxy.doFilter 最外层 finally
finally {
    // 第二重清理:无论过滤器链内部发生什么,上下文必定被清空
    this.securityContextHolderStrategy.clearContext();
    request.removeAttribute(FILTER_APPLIED);
}

5.4 为什么必须设计"双重清理"?

在 Web 服务器中,线程是被池化复用的。Spring Security 的"双重清理"机制应对以下几种风险场景:

  1. SecurityContextHolderFilter 不在过滤器链中 :如果用户自定义配置时移除了 SecurityContextHolderFilter(例如使用了仅含自定义过滤器的链),但 SecurityContext 仍可能被其他方式设置(如通过 SecurityContextHolderFilter 的替代实现),此时必须由 FilterChainProxy 兜底清理。
  2. 极端的 JVM 异常 :虽然 try-finally 在 Java 语义上保证 finally 块的执行,但在 ThreadDeathOutOfMemoryError 等极端场景下,finally 可能不完整执行。外层兜底提供了第二道防线。
  3. 防御性编程 :作为安全框架的"总入口",FilterChainProxy 必须确保无论过滤器链内部发生什么,请求结束前上下文必定清空------这是一种"不信任下游"的防御编程思想。

场景澄清 :如果在 SecurityContextHolderFilter 之前 抛出异常(如早期自定义过滤器异常),此时 SecurityContext 尚未被加载,不存在泄漏风险。真正的双重清理价值在于多层防御:即使某一层清理失效,另一层仍能兜底。


六、完整请求处理流程总结(6.5.x 终极版)

阶段 组件 操作
1 Servlet 容器 收到 HTTP 请求,进入 FilterChain
2 DelegatingFilterProxy 查找名为 springSecurityFilterChain 的 Bean,委托给 FilterChainProxy
3 FilterChainProxy 检查 FILTER_APPLIED 防重入标记
4 FilterChainProxy doFilterInternalHttpFirewall.getFirewalledRequest() 校验请求
5 FilterChainProxy doFilterInternalHttpFirewall.getFirewalledResponse() 包装响应
6 FilterChainProxy getFilters() 路由匹配(短路机制,首个匹配即生效)
7a FilterChainProxy (分支A) 无匹配链 → firewallRequest.reset() → 直接调用原始 Servlet FilterChain
7b VirtualFilterChain (分支B) FilterChainDecorator.decorate(reset, filters) 构造 VirtualFilterChain
8 VirtualFilterChain 递归驱动安全过滤器(currentPosition 指针递增)
9 SecurityContextHolderFilter 防重入检查 → loadDeferredContextsetDeferredContext → 驱动后续过滤器
10 Security Filters 依次执行:CsrfFilter、AuthenticationFilter、AuthorizationFilter 等
11 VirtualFilterChain 所有安全过滤器执行完毕 → 调用 originalChain.doFilter() → 触发 reset Lambda
12 reset Lambda firewallRequest.reset() 恢复原始路径 → chain.doFilter() 交还 Servlet 容器
13 业务 Servlet/Controller 处理实际业务逻辑
14 SecurityContextHolderFilter finally 块 → 第一重清理clearContext() + 移除 FILTER_APPLIED
15 FilterChainProxy catch 块:捕获 RequestRejectedExceptionRequestRejectedHandler.handle()
16 FilterChainProxy finally 块 → 第二重清理clearContext() + 移除 FILTER_APPLIED

下一篇预告:将深入拆解 AuthenticationManager 的创建过程------AuthenticationConfiguration 如何自动加载全局配置器,InitializeUserDetailsBeanManagerConfigurer 如何自动创建 DaoAuthenticationProvider,以及 UserDetailsServiceAutoConfiguration 的条件装配逻辑。

相关推荐
iOS开发上架哦3 小时前
Jenkins 自动上传 IPA 到 App Store 把发布步骤融入 CI/CD
后端·ios
神奇小汤圆3 小时前
告别“大泥球”:我在 Spring Boot 单体架构中实践的模块化隔离
后端
长大19883 小时前
Python 新手最容易踩的 10 个语法坑
后端
二月龙3 小时前
Python 迭代器与生成器精讲:大幅降低内存占用
后端
AINative软件工程4 小时前
Tool Schema 写得好,模型少出错:5 个工程师必知的设计原则
后端·openai
AINative软件工程4 小时前
AI 写的代码,Review 要怎么改?我们团队的 15 条 PR 检查清单
后端·openai
武子康4 小时前
Java-21 深入浅出 MyBatis 手写ORM框架2 手写Resources、MappedStatment、XMLBuilder等
java·后端
techdashen4 小时前
在 Fly.io 上使用 Rust 构建远程开发环境:从 Tokio 到 eBPF
开发语言·后端·rust
摇滚侠4 小时前
Spring 零基础入门到进阶 面向切面 AOP 52-60
java·后端·spring