Spring Web 安全深度:CORS、CSRF 与安全响应头

4 阅读23分钟

概述

在前面的《拦截器、过滤器与 Spring Security 协作机制》一文中,我们已经厘清了 Filter、HandlerInterceptor 以及 Spring Security 过滤器链的执行顺序和分工。本文进一步聚焦于 Spring Web 层中最常见的三类安全机制——CORS、CSRF 防护与安全响应头——深入分析它们在过滤器链中的具体作用方式、源码实现以及适配不同部署架构时的最佳策略。

无论 RESTful API 还是前后端分离应用,跨域请求、CSRF 攻击与不安全的 HTTP 响应头都是 Web 安全的基本威胁。Spring 体系通过 CorsFilterCsrfFilterHeaderWriterFilter 等过滤器,以及 @CrossOrigin 注解等扩展点,为开发者提供了一套从容器层到框架层的渐进式安全防护体系。然而,频繁出现的预检失败、CSRF Token 丢失以及前后端分离环境下的安全误报,往往源于对底层机制的误解。本文将打破“黑盒式”的安全配置,从请求到响应的完整链路上,细致剖析这些安全功能的设计原理与代码实现,帮助专家在复杂环境中做出准确的安全决策。

核心要点

  • CORS 的预检与处理DefaultCorsProcessor 如何根据 OriginAccess-Control-Request-Method 等头进行预检,以及如何为实际请求添加 Access-Control-Allow-Origin
  • CSRF 防护的存储与校验CsrfFilter 如何根据 CsrfTokenRepository 存储和匹配 Token,以及前后端分离场景下的正确配置方式。
  • 安全响应头注入SecurityHeadersFilterHeaderWriterFilter 自动添加的各类安全头及其作用。
  • 异常处理衔接:CORS 拒绝和 CSRF 异常如何被 ExceptionTranslationFilter 转换,并进入统一的错误处理体系(与系列第 6 篇《Spring Web 异常处理全景》呼应)。
  • 设计模式与扩展点:通过策略模式实现 CORS 处理器和 CSRF Token 存储的灵活定制。

文章组织架构图

flowchart TD
    1[1.Web 安全防护全景: 过滤器定位] --> 2[2.跨域资源共享CORS 源码剖析]
    1 --> 3[3.跨站请求伪造CSRF 底层机制]
    1 --> 4[4.安全响应头自动注入与定制]
    2 --> 5[5.前后端分离场景下的策略调整]
    3 --> 5
    4 --> 5
    5 --> 6[6.生产事故排查专题]
    5 --> 7[7.面试高频专题]
    6 --> 7

架构图说明

  • 总览说明:全文 7 个模块从三大安全机制的过滤器位置出发,深入剖析各自的原理、源码与配置,再汇总到前后端分离的策略调整,最后通过事故与面试完成闭环。
  • 逐模块说明:模块 1 建立安全过滤器在请求处理全链路中的位置图;模块 2-4 逐一深挖三大机制;模块 5 聚焦实际部署中的变化;模块 6-7 落回实践与应试。
  • 关键结论理解 CORS 的预检决策树、CSRF Token 的存储与匹配逻辑,以及安全头的注入时机,是掌控 Spring Web 安全边界的关键。

1. Web 安全防护全景:CORS、CSRF 与安全响应头的过滤器定位

Spring Security 在 Web 层构建了一条由多个过滤器组成的责任链,每个过滤器只处理自己关心的安全任务。对于 CORS、CSRF 和安全响应头,主要涉及三个核心过滤器:CorsFilterCsrfFilterHeaderWriterFilter。它们在过滤器链中的默认顺序及与 ExceptionTranslationFilterDispatcherServlet 的相对位置,可以通过 Spring Security 的内部排序机制来理解。

在 Spring Security 5.x 中,过滤器的顺序由 SecurityPropertiesFilterOrderRegistration 决定。默认顺序大致如下:

  • ChannelProcessingFilter (order: -100)
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter (order: -200, 比多数过滤器靠前)
  • CorsFilter (由 CorsConfigurer 添加到 Spring Security 链中,通常在 HeaderWriterFilter 之后,但在 CsrfFilter 之前)
  • CsrfFilter (order: -100, 但在 CorsFilter 之后)
  • LogoutFilter
  • UsernamePasswordAuthenticationFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • ...

需要注意的是,这里讨论的 CorsFilter 是 Spring Security 通过 CorsConfigurer 自动添加的一个过滤器实例,它依赖 CorsConfigurationSource Bean。它与 Spring MVC 提供的 CorsFilter (org.springframework.web.filter.CorsFilter) 不同,后者是独立的 Servlet Filter,通常配置在 Spring Security 链之前。在 Spring Boot 自动配置中,如果存在 CorsConfigurationSource Bean,Spring Security 就会添加自己的 CorsFilter 到过滤链中,从而在安全层面处理 CORS,使得 CORS 错误(如非法 Origin)能被 ExceptionTranslationFilter 统一捕获。

插入安全过滤器在全链路中的位置图:

flowchart TB
    A["客户端请求"] --> B["Servlet 容器"]
    B --> C["FilterChainProxy"]
    C --> D["ChannelProcessingFilter"]
    D --> E["HeaderWriterFilter"]
    E --> F["CorsFilter (Spring Security)"]
    F --> G["CsrfFilter"]
    G --> H["其他认证过滤器"]
    H --> I["ExceptionTranslationFilter"]
    I --> J["FilterSecurityInterceptor"]
    J --> K["DispatcherServlet"]
    K --> L["HandlerMapping"]
    L --> M["HandlerAdapter"]
    M --> N["业务处理"]
    N --> O["响应返回"]

    classDef security fill:#fff4e6,stroke:#ff9800,stroke-width:2px,color:#333;
    classDef mvc fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,color:#0d47a1;
    classDef default fill:#f8f9fa,stroke:#333,stroke-width:1px,color:#333;

图表主旨概括:展现从客户端请求到达 Servlet 容器,经过 Spring Security 过滤器链的各个关键节点,最终进入 DispatcherServlet 的全过程,强调 CORS、CSRF 和安全响应头处理的位置。

逐层/逐元素分解

  • HeaderWriterFilter:在所有过滤器之前(或靠前)将安全响应头写入 HttpServletResponse,确保后续任何处理都不会遗漏这些头。
  • CorsFilter:紧接着处理跨域请求,校验预检请求,为实际请求添加响应头。它位于 CsrfFilter 之前,因为预检请求不应被 CSRF 拦截。
  • CsrfFilter:在 CORS 校验通过后,检查状态修改请求(POST、PUT、DELETE 等)是否携带合法的 CSRF Token。
  • ExceptionTranslationFilter:捕获由后续过滤器(如 FilterSecurityInterceptorCsrfFilter 本身)抛出的异常,并转换成适当的 HTTP 响应(403、401 等)。

设计原理映射:整个过滤器链是典型的责任链模式,每个过滤器只处理特定关注点,可插拔、可排序。HeaderWriterFilter 使用策略模式委托给一系列 HeaderWriter 实现来写入不同响应头。CorsFilter 使用策略模式委托 CorsProcessor 完成实际跨域检查。

工程联系与关键结论理解过滤器的顺序至关重要。若通过传统 Servlet Filter 方式配置 CORS,该 Filter 会位于 Spring Security 链之外,当 CORS 拒绝发生时无法被 ExceptionTranslationFilter 捕获,导致客户端收到原始 403 错误而非标准的 JSON 错误响应。因此,在前后端分离实践中,推荐通过 Spring Security 的 CorsConfigurationSource Bean 来统一管理 CORS,使 CORS 处理落入安全链的统一异常处理体系内。

2. 跨域资源共享(CORS)的源码深度剖析

CORS 协议区分了简单请求和预检请求。简单请求满足:方法为 GET、HEAD、POST,Content-Type 为 application/x-www-form-urlencodedmultipart/form-datatext/plain,且没有自定义头。不满足以上任意条件的请求,浏览器都会先发送一个 OPTIONS 预检请求,询问服务器是否允许实际请求的跨域操作。

2.1 CORS 配置的解析与存储

在 Spring MVC 中,我们可以通过 @CrossOrigin 注解或全局 CorsRegistry 来配置 CORS。这些配置最终会被 AbstractHandlerMapping 收集并转化成 CorsConfiguration 对象。关键代码位于 AbstractHandlerMapping.getHandler( HttpServletRequest request) 中:

// org.springframework.web.servlet.handler.AbstractHandlerMapping
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }
    // Bean name or resolved handler?
    if (handler instanceof String) {
        String handlerName = (String) handler;
        handler = obtainApplicationContext().getBean(handlerName);
    }

    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);

    // ... 省略 CORS 处理部分 ...

    if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
        CorsConfiguration config = (this.corsConfigurationSource != null ? 
                                     this.corsConfigurationSource.getCorsConfiguration(request) : null);
        CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
        config = (config != null ? config.combine(handlerConfig) : handlerConfig);
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }
    return executionChain;
}
  • getCorsConfiguration(handler, request) 方法会根据处理器类型(HandlerMethod)上的 @CrossOrigin 注解和类级别注解解析出 CorsConfiguration。解析工作由 CorsAnnotationParser 完成,它将注解属性如 originsmethodsallowedHeaders 等填充到 CorsConfiguration 对象。
  • 处理器配置与全局 CorsConfigurationSource(可自定义 Bean)合并后,通过 getCorsHandlerExecutionChain 将配置注入到执行链中。该方法会返回一个带 CORS 配置的 HandlerExecutionChain,如果需要,还会在链中添加一个 CorsInterceptor(对于 Spring MVC,CORS 处理主要由 AbstractHandlerMapping 内嵌的 CorsProcessor 完成,而 Spring Security 的 CorsFilter 则独立执行)。

在 Spring Security 端,通过 CorsConfigurer 添加的 CorsFilter 会使用 CorsConfigurationSource Bean 提供配置。典型配置如下:

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
    configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

2.2 DefaultCorsProcessor 处理流程

无论是 Spring Security 的 CorsFilter 还是 Spring MVC 的 HandlerMapping,最终都委托给 CorsProcessor 接口,默认实现为 DefaultCorsProcessor。该处理器的 processRequest 方法如下:

// org.springframework.web.cors.DefaultCorsProcessor
@Override
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
                              HttpServletResponse response) throws IOException {

    response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
    if (!CorsUtils.isCorsRequest(request)) {
        return true;
    }
    if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
        // 已经处理过,直接放行
        return true;
    }
    boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
    if (config == null) {
        if (preFlightRequest) {
            rejectRequest(new ServletServerHttpResponse(response));
            return false;
        }
        return true;
    }
    return handleInternal(new ServletServerHttpRequest(request), 
                          new ServletServerHttpResponse(response), config, preFlightRequest);
}

该方法首先添加 Vary: Origin 头,提示客户端按 Origin 头缓存响应。然后判断:

  • 如果不是 CORS 请求(没有 Origin 头),则直接通过。
  • 如果是预检请求,但没有任何 CORS 配置,则拒绝。
  • 最终进入 handleInternal 方法进行详细的头校验和响应头组装。

CORS 预检请求与实请求的处理序列图

sequenceDiagram
    participant Browser
    participant CorsFilter
    participant DefaultCorsProcessor
    participant CorsConfiguration

    Note over Browser, CorsFilter: 预检OPTIONS请求
    Browser->>CorsFilter: OPTIONS /api/data 携带 Origin、Access-Control-Request-Method
    CorsFilter->>DefaultCorsProcessor: processRequest
    DefaultCorsProcessor->>DefaultCorsProcessor: 检查是否为CORS请求
    DefaultCorsProcessor->>CorsConfiguration: 获取匹配的配置
    alt 配置存在且允许该Origin
        DefaultCorsProcessor->>DefaultCorsProcessor: handleInternal 校验 Method/Headers
        DefaultCorsProcessor->>Browser: 200 OK 添加 Access-Control-Allow-Origin、Allow-Methods等
        Browser->>Browser: 预检通过,发送实际请求
    else 未配置或拒绝
        DefaultCorsProcessor->>Browser: 403 Forbidden
    end

    Note over Browser, CorsFilter: 实际请求(如 GET)
    Browser->>CorsFilter: GET /api/data 携带 Origin
    CorsFilter->>DefaultCorsProcessor: processRequest
    DefaultCorsProcessor->>CorsConfiguration: 获取配置
    alt 允许
        DefaultCorsProcessor->>Browser: 200 OK 添加 Access-Control-Allow-Origin、Expose-Headers等
    else 拒绝
        DefaultCorsProcessor->>Browser: 403 Forbidden
    end

图表主旨概括:详细描绘了浏览器发起预检请求和实际 CORS 请求时,CorsFilter 如何与 DefaultCorsProcessor 交互,并根据 CorsConfiguration 做出允许或拒绝的决定。

逐层/逐元素分解

  • 预检请求阶段:DefaultCorsProcessor.processRequest 被调用,检测到 OPTIONS 方法和 Access-Control-Request-Method 头。从 CorsConfiguration 获取与路径匹配的配置,依次检查 Origin 是否允许、Request-Method 是否允许、Request-Headers 是否允许,全部通过后写入相应的 Access-Control-Allow-* 头。
  • 实际请求阶段:同样进入 processRequest,但验证逻辑简单,主要检查 Origin 并添加 Access-Control-Allow-OriginAccess-Control-Expose-Headers
  • handleInternal 方法内使用策略“允许所有 Origins”或“具体 Origin”来决定是否回显 Origin 头。

设计原理映射CorsProcessor 是策略接口,DefaultCorsProcessor 完成了基于 W3C CORS 规范的算法实现。CorsConfiguration 充当上下文对象,封装了允许的源、方法和头信息。UrlBasedCorsConfigurationSource 使用路径模式映射来选择合适的配置,体现了策略模式

工程联系与关键结论CORS 处理必须区分预检请求和实际请求。许多开发者在配置 CorsFilter 时没有正确设置预检请求的缓存时间 maxAge,导致每次请求前都发送预检,增加延迟。合理的 maxAge 配置可让浏览器在有效期内缓存预检结果,直接从实际请求开始,显著提升性能。

2.3 与 Spring Security 的整合

Spring Security 通过 CorsConfigurerCorsFilter 插入过滤器链。如果用户定义了 CorsConfigurationSource Bean,则 Spring Security 的自动配置 (WebSecurityConfigurerAdapter 相关) 会自动使用它。核心源码来自 CorsConfigurer

public class CorsConfigurer<H extends HttpSecurityBuilder<H>> 
    extends AbstractHttpConfigurer<CorsConfigurer<H>, H> {
    @Override
    public void configure(H http) {
        CorsFilter corsFilter = new CorsFilter(corsConfigurationSource);
        http.addFilter(corsFilter);
    }
}

CorsFilter (org.springframework.web.filter.CorsFilter) 会遍历请求,并调用 CorsProcessor 处理。它确保在过滤器层面拦截所有请求,包括那些不会到达 Spring MVC 的请求(如静态资源)。

3. 跨站请求伪造(CSRF)防护的底层机制

CSRF 攻击依赖于浏览器在跨站请求中自动携带目标站点的 Cookie(特别是会话 Cookie)。Spring Security 通过“同步令牌模式”防护:服务器生成一个随机 Token,将其存储在 Session(或 Cookie)中,并在表单或请求头中要求客户端提交相同的 Token。攻击者无法读取同源策略限制下的 Token,因此攻击失败。

3.1 CsrfFilter 的工作流程

CsrfFilter 是履行 CSRF 校验的过滤器,其 doFilterInternal 方法骨架如下:

// org.springframework.security.web.csrf.CsrfFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                FilterChain filterChain) throws ServletException, IOException {
    CsrfToken csrfToken = this.tokenRepository.loadToken(request);
    boolean missingToken = (csrfToken == null);
    if (missingToken) {
        csrfToken = this.tokenRepository.generateToken(request);
        this.tokenRepository.saveToken(csrfToken, request, response);
    }
    request.setAttribute(CsrfToken.class.getName(), csrfToken);
    request.setAttribute(csrfToken.getParameterName(), csrfToken);

    if (!this.requireCsrfProtectionMatcher.matches(request)) {
        filterChain.doFilter(request, response);
        return;
    }

    String actualToken = request.getHeader(csrfToken.getHeaderName());
    if (actualToken == null) {
        actualToken = request.getParameter(csrfToken.getParameterName());
    }
    if (!csrfToken.getToken().equals(actualToken)) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Invalid CSRF token found for %s", 
                                                request.getRequestURL()));
        }
        boolean missingTokenAttribute = (actualToken == null);
        if (missingTokenAttribute) {
            this.logger.debug("No CSRF token found for " + request.getRequestURL());
        }
        throw new MissingCsrfTokenException(actualToken);
    }
    filterChain.doFilter(request, response);
}

解读

  • requireCsrfProtectionMatcher 默认为 DefaultRequiresCsrfMatcher,它会对“状态修改”的 HTTP 方法(POST、PUT、DELETE、PATCH)进行匹配,而 GET、HEAD、TRACE、OPTIONS 则放行。这是标准实践,因为 CSRF 攻击主要针对状态改变请求。
  • Token 加载策略:先从 tokenRepository 加载 Token,如果不存在则生成并保存,保证了 Token 的可用性。
  • 校验逻辑:尝试从请求头 csrfToken.getHeaderName()(默认为 X-CSRF-TOKEN)或请求参数 _csrf 中获取实际 Token,与存储的 Token 进行字符串对比。不匹配或缺失则抛出 MissingCsrfTokenException

3.2 CsrfTokenRepository 存储策略

CsrfTokenRepository 接口负责 Token 的持久化和加载。关键实现有三:

  1. HttpSessionCsrfTokenRepository:将 Token 保存在 HttpSession 的属性中(键为 HttpSessionCsrfTokenRepository.CSRF_TOKEN_ATTR_NAME)。适用于传统服务端渲染应用,Session 共享稳定。
  2. CookieCsrfTokenRepository:将 Token 保存在一个名为 XSRF-TOKEN 的 Cookie 中(可通过配置修改)。默认设置为 HttpOnly: true,即 JavaScript 不可读取。但若需前端读取并放入请求头,必须调用 withHttpOnlyFalse() 禁用 HttpOnly,从而允许 JS 读写该 Cookie(注意这会增加 XSS 攻击下的风险)。
  3. LazyCsrfTokenRepository:包装了另一个 CsrfTokenRepository,它延迟生成 Token,直到实际需要时(即第一次调用 getToken())才生成并保存,避免无谓的 Token 创建开销,优化性能。

LazyCsrfTokenRepository 的实现运用了懒加载代理模式,延迟生成 Token:

@Override
public CsrfToken loadToken(HttpServletRequest request) {
    CsrfToken token = this.delegate.loadToken(request);
    if (token == null) {
        return null;
    }
    return new LazyCsrfToken(this.delegate, request);
}
// LazyCsrfToken 内部类
private static class LazyCsrfToken implements CsrfToken {
    private final CsrfTokenRepository delegate;
    private final HttpServletRequest request;
    private String tokenValue;
    // ...
    @Override
    public String getToken() {
        if (this.tokenValue == null) {
            this.tokenValue = this.delegate.loadToken(request).getToken();
            // 确保已保存...
        }
        return this.tokenValue;
    }
}

3.3 CSRF Token 的生成、存储、校验与异常抛出序列图

sequenceDiagram
    participant Client
    participant CsrfFilter
    participant LazyCsrfTokenRepository
    participant HttpSessionCsrfTokenRepository

    Client->>CsrfFilter: POST /transfer 携带 Cookie (SessionId)
    CsrfFilter->>LazyCsrfTokenRepository: loadToken(request)
    LazyCsrfTokenRepository->>HttpSessionCsrfTokenRepository: loadToken(request)
    HttpSessionCsrfTokenRepository-->>LazyCsrfTokenRepository: CsrfToken (or null)
    alt Token 不存在
        LazyCsrfTokenRepository-->>CsrfFilter: 返回 null
        CsrfFilter->>HttpSessionCsrfTokenRepository: generateToken(request)
        HttpSessionCsrfTokenRepository-->>CsrfFilter: 新 CsrfToken
        CsrfFilter->>HttpSessionCsrfTokenRepository: saveToken(token, request, response)
        CsrfFilter->>request: setAttribute(CsrfToken.class.getName(), token)
    else Token 存在
        LazyCsrfTokenRepository-->>CsrfFilter: LazyCsrfToken 包装
    end
    CsrfFilter->>CsrfFilter: requireCsrfProtectionMatcher.matches 为 true (POST)
    CsrfFilter->>request: getHeader("X-CSRF-TOKEN") 或 getParameter("_csrf")
    alt token 匹配
        CsrfFilter->>FilterChain: doFilter 放行
    else token 缺失或不匹配
        CsrfFilter->>CsrfFilter: throw MissingCsrfTokenException
    end
    Note over CsrfFilter: ExceptionTranslationFilter 捕获并返回 403

图表主旨概括:展示 CsrfFilter 如何通过 CsrfTokenRepository 进行 Token 的惰性加载或生成,并对需要保护的请求进行实际校验,以及令牌不匹配时的异常处理路径。

逐层/逐元素分解

  • LazyCsrfTokenRepository 作为装饰器,只有当调用 getToken() 时才会从真实 Repository 加载 Token,生成并写入响应 Cookie,从而延迟了 Token 的实际生成。
  • 校验阶段分别从自定义请求头或请求参数读取 Token,优先从头部读取,这适应前后端分离架构。
  • 当校验失败,抛出 MissingCsrfTokenException,该异常会被 Spring Security 的 ExceptionTranslationFilter 捕获,最终转换为 403 Forbidden 响应。

设计原理映射CsrfTokenRepository策略模式,允许根据部署环境选择存储介质。LazyCsrfTokenRepository装饰器模式/代理模式应用,在不改变接口的情况下增加延迟加载行为。DefaultRequiresCsrfMatcher策略模式用于判定哪些请求需要保护。

工程联系与关键结论在前后端分离且使用 Session 的混合场景中,若 CookieCsrfTokenRepository 的 HttpOnly 未关闭,JavaScript 无法读取 CSRF Token 并设置到请求头,导致每次状态修改请求都因 Token 缺失而失败。遇到 CSRF 403 时,首先检查 Cookie 中的 XSRF-TOKEN 是否存在且 JavaScript 能够读取。

3.4 认证/登出时的 Token 清理

CsrfAuthenticationStrategy:当用户认证成功后,该策略会清除旧 Token 并生成新 Token 保存在 Session 中(通过调用 CsrfTokenRepository.saveToken(...)),以防止会话固定攻击(Session Fixation)组合 CSRF。而 CsrfLogoutHandler 在登出时同样会清除 CSRF Token。

4. 安全响应头的自动注入与定制

Spring Security 默认通过 HeaderWriterFilter 向每个响应注入一系列安全相关的 HTTP 头。在 Spring Boot 2.7.x 的自动配置中,由 SecurityHeadersAutoConfiguration 负责组装这些 HeaderWriter 实例。核心逻辑可从 SecurityHeadersAutoConfiguration 中窥见:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityHeadersAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public HeaderWriterFilter headerWriterFilter() {
        List<HeaderWriter> writers = new ArrayList<>();
        // 添加默认HeaderWriter
        writers.add(new XContentTypeOptionsHeaderWriter());
        writers.add(new XXssProtectionHeaderWriter());
        writers.add(new CacheControlHeadersWriter());
        writers.add(new XFrameOptionsHeaderWriter());
        writers.add(new HstsHeaderWriter());
        // ...
        return new HeaderWriterFilter(writers);
    }
}

Spring Security 5.x 本身也提供了 HeadersConfigurer 进行细粒度定制,例如:

http.headers()
    .contentTypeOptions()
    .and()
    .xssProtection()
    .and()
    .cacheControl()
    .and()
    .frameOptions().deny()
    .and()
    .httpStrictTransportSecurity()
        .maxAgeInSeconds(31536000)
        .includeSubDomains(true);

安全响应头注入的序列图

sequenceDiagram
    participant Client
    participant HeaderWriterFilter
    participant HstsHeaderWriter
    participant XContentTypeOptionsHeaderWriter
    participant Response
    participant FilterChain

    Client->>HeaderWriterFilter: 任何请求
    HeaderWriterFilter->>HeaderWriterFilter: 遍历 HeaderWriter 列表
    HeaderWriterFilter->>HstsHeaderWriter: writeHeaders(request, response)
    HstsHeaderWriter->>Response: addHeader("Strict-Transport-Security", "max-age=...")
    HeaderWriterFilter->>XContentTypeOptionsHeaderWriter: writeHeaders(request, response)
    XContentTypeOptionsHeaderWriter->>Response: addHeader("X-Content-Type-Options", "nosniff")
    Note over HeaderWriterFilter,Response: 类似写入其他头: X-XSS-Protection, Cache-Control 等
    HeaderWriterFilter->>FilterChain: doFilter 放行

图表主旨概括HeaderWriterFilter 接管响应对象,通过一组 HeaderWriter 实现,将安全相关的 HTTP 头添加到响应中,所有经过该过滤器的响应都会自动包含这些头。

逐层/逐元素分解

  • HstsHeaderWriter 写入 Strict-Transport-Security(仅当请求为 HTTPS 时,通常配置为只在 HTTPS 下注入),强制浏览器在指定时间内只通过 HTTPS 访问该域名。
  • XContentTypeOptionsHeaderWriter 写入 X-Content-Type-Options: nosniff,防止浏览器 MIME 嗅探。
  • XXssProtectionHeaderWriter 写入 X-XSS-Protection: 1; mode=block,开启浏览器内置反射型 XSS 过滤器。
  • CacheControlHeadersWriter 写入 Cache-Control: no-cache, no-store, max-age=0, must-revalidate 等,确保敏感页面不被缓存。

设计原理映射HeaderWriterFilter 聚合了多个 HeaderWriter,而 HeaderWriter 接口定义了 writeHeaders(HttpServletRequest request, HttpServletResponse response) 方法,体现了策略模式HeadersConfigurer 使用了建造者模式来简化配置。

工程联系与关键结论安全响应头注入应在过滤器链的前端完成,使得即使请求在后继过滤器中被拒绝,响应头仍能被添加,增强错误页面的安全性。当需要自定义安全头(如 Content-Security-Policy)时,应实现 HeaderWriter 接口并注册到 Spring 容器,或通过 HeadersConfigurer.addHeaderWriter(...) 方法添加,避免完全覆盖默认安全头而失去基本保护。

4.1 关键 HeaderWriter 实现浅析

HstsHeaderWriter 负责生成 HSTS 头,其 writeHeaders 方法检查请求是否为 HTTPS(可通过 hstsHeaderWriter.setForceHttps(true) 强制),并且 HttpServletResponse 尚未提交,然后写入头。如果请求是非 HTTPS,默认不写,防止 HSTS 头在不安全的连接上被恶意注入。

自定义 Content-Security-Policy 至关重要。CSP 通过指令限制页面可以加载的资源来源,可有效防御 XSS 攻击。可以这样注册一个自定义 CSP 的 HeaderWriter

@Bean
public HeaderWriter contentSecurityPolicyHeaderWriter() {
    return new StaticHeadersWriter("Content-Security-Policy",
        "default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted-scripts.example.com");
}

5. 前后端分离场景下的安全策略调整

5.1 CORS 策略

前后端分离中,前端常运行在独立域(如 http://localhost:3000)。推荐做法是不再依赖 @CrossOrigin 注解,而是通过 CorsConfigurationSource Bean 全局配置,以与 Spring Security 无缝整合:

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOriginPatterns(List.of("http://localhost:[*]", "https://*.example.com"));
    config.setAllowCredentials(true); // 允许携带 Cookie/认证信息
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("*"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

注意,当 allowCredentialstrue 时,Access-Control-Allow-Origin 不能为 *,必须使用具体 Origin 或模式匹配。

5.2 CSRF 方案转型

在纯 API 的无状态 JWT 认证中,由于不依赖 Session,CSRF 攻击无从依附,因为攻击者无法在跨站请求中携带 Authorization: Bearer ... 请求头。因此,这类项目可以安全地关闭 CSRF 防护:http.csrf().disable()。然而,如果 API 仍使用 Cookie 进行认证(如通过 Session Cookie),则 CSRF 防护依然必要。

此时,可使用 Cookie 到 Header 的双重提交模式。前端从 Cookie 中读取 CSRF Token(要求 CookieCsrfTokenRepository 设置 withHttpOnlyFalse()),并将其放入状态修改请求的自定义头 X-CSRF-TOKEN 中。CsrfFilter 校验 Header 中的 Token 与 Cookie 中的 Token 是否一致。因为同源策略使得攻击者无法在跨站请求中设置请求头,所以该方案有效。

前后端分离架构下 CSRF 方案的对比图:

flowchart LR
    subgraph Session ["Session模式"]
        A1["浏览器"] -->|"Cookie: JSESSIONID"| B1["服务器"]
        B1 -->|"存储Token于HttpSession"| C1["CsrfToken"]
        A1 -->|"POST + 参数_csrf"| B1
    end
    subgraph Cookie ["Cookie模式(HttpOnlyFalse)"]
        A2["浏览器"] -->|"Cookie: XSRF-TOKEN"| B2["服务器"]
        B2 -->|"读取XSRF-TOKEN Cookie"| C2["CsrfToken"]
        A2 -->|"POST + Header: X-CSRF-TOKEN"| B2
        B2 -->|"比对Cookie与Header Token"| D2{"校验"}
    end
    subgraph JWT ["JWT无状态"]
        A3["浏览器"] -->|"Authorization: Bearer jwt"| B3["服务器"]
        B3 -->|"无Session,无CSRF风险"| C3["CSRF关闭"]
    end

图表主旨概括:对比了经典 Session 方式、Cookie-to-Header 双重提交方式和无状态 JWT 方式下 CSRF 防护的差异。

逐层/逐元素分解

  • Session 模式:适合传统同源架构,CSRF Token 存储在服务器端 Session,表单提交时作为隐藏字段。但在前后端分离中因无法直接写入表单而受限。
  • Cookie 模式:服务端将 Token 写入一个非 HttpOnly 的 Cookie,前端读取并写入请求头。此模式要求 Cookie 非 HttpOnly,增加 XSS 风险,但解决了前后端分离的 Token 传递问题。
  • JWT 无状态:认证信息完全由客户端通过请求头携带,跨站请求无法自动附加,CSRF 自然免疫。

设计原理映射:Session 模式和 Cookie 模式均基于同步令牌模式,Token 的传输机制不同,但本质都是验证请求来源的同一性。

工程联系与关键结论选择 CSRF 方案时需权衡安全性与可用性。关闭 withHttpOnlyFalse 可提升 XSS 防护,但需通过后端模板将 Token 注入前端页面;若必须使用纯静态前端且无法安装模板引擎,则可考虑关闭 HttpOnly。最佳实践是使用 JWT 认证,彻底关闭 CSRF,同时确保 JWT 不存储在 LocalStorage 而易受 XSS 攻击,推荐存储在 HttpOnly Cookie 中,并配合 SameSite=Strict 属性。

6. 生产事故排查专题

事故 6.1:CORS 预检请求全部失效,前端控制台 CORS 错误

现象:前端应用升级到新版后,所有 API 请求均被 CORS 策略阻止,浏览器显示 “Access to XMLHttpRequest ... has been blocked by CORS policy: Response to preflight request doesn't pass access control check”。后端日志无 CORS 相关输出。

排查思路

  1. 检查浏览器 Network,发现每个 POST 前都有 OPTIONS 预检请求,但返回状态码 403。
  2. 检查后端 CorsConfigurationSource Bean 配置,发现允许的源设置为 https://app.example.com:443,但前端请求的 Origin 为 https://app.example.com(端口 443 是默认端口,浏览器会自动省略)。浏览器发送 Origin 为 https://app.example.com,而服务器配置的源包含端口,导致匹配失败。
  3. 进一步查看 Spring Security 日志,发现 CorsFilter 打印了 “Rejected: because 'Origin' header value 'app.example.com' is not in allowed origins”。

根因:Origin 匹配过于严格,包含了默认端口,而浏览器标准化 Origin 时会省略默认端口。

解决:修改 CorsConfiguration,将 allowedOrigins 改为 https://app.example.com,或使用 allowedOriginPatterns 支持通配符:"https://*.example.com"

最佳实践:配置 CORS 源时避免端口号一致性问题,尽量使用 setAllowedOriginPatterns() 并利用 Spring 的 OriginPattern,它支持通配符且能正确处理默认端口。开启 spring 框架的 CORS 调试日志:logging.level.org.springframework.web.cors=DEBUG

事故 6.2:微服务拆分后 CSRF Token 频繁丢失,导致表单提交 403

现象:单体架构迁移到 Spring Cloud 微服务后,用户在首页登录后进入管理页面,提交表单时经常收到 403 禁止访问,刷新页面后再提交可能成功。

排查思路

  1. 查看错误响应,发现是 MissingCsrfTokenException
  2. 回顾架构,前端请求通过 API 网关,但后端多个服务实例无 Session 共享。CSRF Token 默认存储在 HttpSession 中,用户请求被负载均衡到不同实例时 Token 丢失。
  3. 查看 CsrfFilter 日志,有的实例生成新 Token 返回给客户端,但客户端拿到的 Token 对应旧实例,导致不匹配。

根因HttpSessionCsrfTokenRepository 依赖 Session 亲和性,在无 Session 共享的微服务集群中,请求路由到不同实例导致 Token 失效。

解决

  • 方案一:配置 CookieCsrfTokenRepository.withHttpOnlyFalse(),将 Token 存储在 Cookie 中,因为 Cookie 由浏览器携带,不依赖服务器实例。
  • 如果认证已从 Session 迁移到 JWT,则可关闭 CSRF 防护:http.csrf().disable()
  • 如果仍要使用 Session,必须实现 Session 共享(如 Spring Session + Redis)。

最佳实践:微服务化时尽早向无状态认证转型,避免 Session 引起的状态依赖。若必须使用 CSRF,选择 CookieCsrfTokenRepository 并配合 SameSite=Strict Cookie 属性增强安全性。

7. 面试高频专题

  1. Spring 如何处理 CORS?@CrossOriginCorsFilter 的区别? @CrossOrigin 是基于注解的方法/类级别配置,通过 AbstractHandlerMapping 解析,仅在到达 Spring MVC 的 Handler 时生效。CorsFilter 是 Servlet Filter,可在更早阶段拦截所有请求(包括静态资源),与 Spring Security 深度整合。两者最终都委托 CorsProcessor 处理。

  2. 什么是 CSRF?Spring Security 是如何实现 CSRF 防护的? CSRF 攻击利用用户已登录的身份,在诱骗请求中自动携带认证 Cookie。Spring Security 通过同步令牌模式防御:服务器生成随机 Token 保存在 Session/Cookie,并要求请求附带相同 Token(参数或请求头)。CsrfFilter 负责校验。

  3. CSRF Token 的存储方式有哪些?各有什么优缺点?

    • HttpSessionCsrfTokenRepository:服务端存储,安全,但依赖 Session 和服务器状态,不适用分布式无 Session 共享环境。
    • CookieCsrfTokenRepository:客户端 Cookie 存储,无状态,但若 HttpOnly 为 true,前端无法读取;若关闭 HttpOnly,则 XSS 下可能泄露。
    • LazyCsrfTokenRepository:延迟加载装饰器,性能优化,可包装上述任意实现。
  4. 前后端分离后,CSRF 防护应该怎么调整? 若使用 JWT 等无状态认证(Token 在请求头),CSRF 不再构成威胁,可关闭防护。若仍使用 Cookie/Session,则必须改用 CookieCsrfTokenRepository 并设置 withHttpOnlyFalse() 以让 JS 读取,并配合 SameSite=Strict 提升安全。

  5. X-Content-Type-OptionsContent-Security-Policy 头有什么作用? X-Content-Type-Options: nosniff 阻止浏览器进行 MIME 类型嗅探,强制按服务器声明的 Content-Type 处理资源,防止脚本注入。Content-Security-Policy 是一套指令集,限制页面可以加载的资源的来源(脚本、样式、图片等),是防御 XSS 的核心手段。

  6. 如果前端向后端发送 AJAX 请求时发现 CORS 错误,可能是什么原因?怎样排查? 常见原因:Origin 不匹配、HTTP 方法不在允许列表、预检请求失败、后端未返回正确响应头、Credentials 模式与 Access-Control-Allow-Origin: * 冲突等。排查:检查浏览器 Network 预检请求响应头,查看 Spring Security CORS 调试日志,确认 CorsConfiguration 配置正确,检查过滤器顺序。

  7. 如何自定义 Spring Security 的安全响应头? 实现 HeaderWriter 接口或在配置中使用 StaticHeadersWriter,然后通过 http.headers().addHeaderWriter(...) 添加。也可以直接提供 HeaderWriter Bean,Spring Boot 会自动将其添加到 HeaderWriterFilter 中。

  8. CsrfFilter 校验不通过时,Spring Security 会怎么处理?与统一异常处理体系如何衔接? CsrfFilter 抛出 MissingCsrfTokenException,该异常被 ExceptionTranslationFilter 捕获,然后调用 AuthenticationEntryPoint,默认发送 403,并可能转发至错误页面或触发 @ExceptionHandler 机制(取决于配置)。

  9. 为什么对 API 项目来说,有时建议关闭 CSRF 防护? 若 API 使用无状态 Bearer Token 认证,攻击者无法在跨站请求中伪造 Authorization 请求头,不存在 CSRF 威胁。关闭 CSRF 可简化配置,减少不必要的 Token 校验开销。

  10. CORS 预检请求的流程以及处理过程中用到的核心类有哪些? 浏览器发出 OPTIONS 请求,携带 Origin, Access-Control-Request-Method 和可能的 Access-Control-Request-HeadersCorsFilter 调用 DefaultCorsProcessor.processRequest,检查 CorsConfiguration 允许性,然后返回对应的 Access-Control-Allow-* 头部。核心类:CorsUtils, DefaultCorsProcessor, UrlBasedCorsConfigurationSource

  11. 如何彻底禁用 Spring Security 自动注入的某些安全响应头? 使用 http.headers().defaultsDisabled() 禁用所有默认头,然后手动添加需要的头;或使用特定的禁用方法如 http.headers().frameOptions().disable().httpStrictTransportSecurity().disable()

  12. (系统设计题)设计一个支持同源和跨域访问的 API 网关,网关需对管理后台(同源)实施严格的 CSRF 防护,对第三方 API 调用(跨域)仅启用 CORS 校验,同时统一管理安全响应头。请给出过滤器的编排方案和核心实现思路。

    • 网关可基于 Spring Cloud Gateway 或 Zuul,在路由层使用 Spring Security 配置。
    • 使用两个安全配置适应不同路径:/admin/** 启动 CSRF 防护(CsrfFilter),要求管理后台同源调用携带 CSRF Token;/api/public/** 配置 CORS (CorsConfigurationSource 允许第三方源),但关闭 CSRF。
    • 通过 CorsConfigurationSource 对公开 API 设置适当的允许源和方法;管理后台源严格限制为自有域名。
    • 安全响应头通过全局 HeaderWriterFilter 统一添加,所有应用均受保护。
    • 过滤器编排:CorsFilter 对所有路径生效,CsrfFilter 仅对 /admin/** 路径生效(使用 RequestMatcher 条件),确保互不干扰。异常处理统一由 ExceptionTranslationFilter 负责。

Spring Web 安全机制速查表

机制核心组件/注解默认行为自定义方式
CORS@CrossOrigin, CorsConfigurationSource, CorsFilter, DefaultCorsProcessor不启用跨域,需显式配置实现 CorsConfigurationSource Bean,或重写 addCorsMappings
CSRFCsrfFilter, CsrfTokenRepository(HttpSessionCsrfTokenRepository/CookieCsrfTokenRepository), CsrfToken默认开启,保护 POST/PUT/DELETE 等http.csrf().disable() 或自定义 CsrfTokenRepository
安全响应头HeaderWriterFilter, HeaderWriter 实现类(HstsHeaderWriter等)自动添加常见安全头http.headers().addHeaderWriter(...) 或提供 HeaderWriter Bean

延伸阅读

  • Spring Security 官方文档 CORS 与 CSRF 章节
  • OWASP CORS 安全指南
  • OWASP CSRF 防护速查表
  • Spring 框架 CORS 支持源码分析

本次深度解析覆盖了 CORS、CSRF 和安全响应头在 Spring Web 层的实现机理,从过滤器链定位,到源码级别的逻辑剖析,再到前后端分离场景下的策略重构,希望为专家读者构建清晰、可落地的安全知识体系。