乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

HeaderWriterFilter

定义与作用

HeaderWriterFilter 是 Spring Security 过滤器链中负责向响应写入安全 HTTP 头的过滤器。它位于 SecurityContextHolderFilter 之后、CorsFilter 之前,在每个响应返回给客户端之前,添加一组安全响应头,以防御常见的 Web 攻击(如 Clickjacking、MIME 类型嗅探、XSS 等)。这些头部由 HeaderWriter 接口实现生成,可灵活组合和自定义。


核心原理

1. HeaderWriter 接口与组合机制

public interface HeaderWriter {
    // 向响应写入安全头部
    void writeHeaders(HttpServletRequest request, HttpServletResponse response);
}

HeaderWriterFilter 内部维护 List<HeaderWriter>,响应提交前逐一调用:

// HeaderWriterFilter 核心逻辑(简化)
public class HeaderWriterFilter extends OncePerRequestFilter {
    private final List<HeaderWriter> headerWriters;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) {
        // 包装响应,在提交前拦截
        HeaderWriterResponse wrappedResponse = 
            new HeaderWriterResponse(request, response, this.headerWriters);
        try {
            filterChain.doFilter(request, wrappedResponse);
        } finally {
            // 确保响应提交前写入头部(即使发生异常)
            wrappedResponse.writeHeaders();
        }
    }
}

2. 内置安全响应头速查

响应头默认值防御目标对应 HeaderWriter
X-Frame-OptionsDENYClickjacking(点击劫持)XFrameOptionsHeaderWriter
X-Content-Type-OptionsnosniffMIME 类型嗅探攻击XContentTypeOptionsHeaderWriter
X-XSS-Protection0禁用浏览器旧版 XSS 过滤器(避免其自身漏洞)XXssProtectionHeaderWriter
Strict-Transport-Securitymax-age=31536000; includeSubDomainsHSTS 强制 HTTPSHstsHeaderWriter
Cache-Controlno-cache, no-store, max-age=0, must-revalidate防止缓存敏感信息CacheControlHeadersWriter
Pragmano-cacheHTTP/1.0 缓存控制CacheControlHeadersWriter
Expires0缓存过期CacheControlHeadersWriter

3. 响应包装与延迟写入机制


完整示例一:自定义 CSP 策略与 Frame 选项

场景说明

应用需要:

  1. 允许同域 iframe 嵌套(如内部管理系统被其他同域页面嵌入),因此 X-Frame-Options 不能是 DENY,需要 SAMEORIGIN 或更精细的 frame-ancestors。
  2. 配置 Content-Security-Policy (CSP) 限制脚本来源,防止 XSS 注入。

操作前配置(默认头部,无法满足需求)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .headers(Customizer.withDefaults());  // 默认 X-Frame-Options: DENY
    return http.build();
}

问题分析:默认 X-Frame-Options: DENY 完全禁止任何 iframe 嵌套,包括同域页面。如果应用有合法的同域 iframe 使用场景(如管理后台被 dashboard 页面嵌入),会被浏览器拒绝加载。此外,默认未配置 CSP,无法限制脚本加载来源,XSS 防护不足。

操作后配置(精细化头部控制)

@Configuration
@EnableWebSecurity
public class CustomSecurityHeadersConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
            .headers(headers -> headers
                // 1. 允许同域 iframe(SAMEORIGIN 替代 DENY)
                .frameOptions(frame -> frame.sameOrigin())
                
                // 2. 配置 Content-Security-Policy
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives(
                        "default-src 'self'; " +
                        "script-src 'self' 'nonce-{random}' cdn.example.com; " +
                        "style-src 'self' 'unsafe-inline'; " +
                        "img-src 'self' data: https:; " +
                        "frame-ancestors 'self'; " +  // 替代 X-Frame-Options 的精细控制
                        "form-action 'self'; " +
                        "base-uri 'self';"
                    )
                )
                
                // 3. 强制 HTTPS(HSTS,仅生产环境启用)
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000)  // 1 年
                    // .preload(true)  // 可选:提交到浏览器预加载列表
                )
                
                // 4. Referrer 策略(控制跳转时 Referer 头)
                .referrerPolicy(referrer -> referrer
                    .policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
                )
                
                // 5. 保留默认的其他头部(X-Content-Type-Options, Cache-Control 等)
                .defaultsDisabled()  // 如果不需要,可禁用默认头部
                // 此处不调用 defaultsDisabled(),保留默认 + 自定义
            );
        return http.build();
    }
}

测试验证与结果分析

响应头值效果
X-Frame-OptionsSAMEORIGIN允许同域页面 iframe 嵌入,阻止跨域嵌入
Content-Security-Policydefault-src 'self'; ...限制资源只能从同源加载,脚本仅允许同源 + 指定 CDN
Strict-Transport-Securitymax-age=31536000; includeSubDomains浏览器 1 年内强制使用 HTTPS 访问本域及子域
Referrer-Policystrict-origin-when-cross-origin同域请求完整 Referrer,跨域请求仅发送 Origin
X-Content-Type-Optionsnosniff浏览器禁止 MIME 类型嗅探(默认保留)
Cache-Controlno-cache, no-store...敏感页面不缓存(默认保留)
# 使用 curl 验证响应头
curl -I -u user:password http://localhost:8080/home
# 观察响应中的 X-Frame-Options, Content-Security-Policy 等头

完整示例二:为不同路径配置差异化的安全头部

场景说明

应用同时提供:

  • Web 管理页面(/web/**):需要完整的 CSP、HSTS、X-Frame-Options 等头部。
  • 图片/文件 API(/files/**):需要放宽 CSP(允许图片加载),但保留 HSTS。
  • 健康检查端点(/health):无需缓存控制和复杂 CSP,但保留 HSTS。

操作前配置(统一头部,导致文件服务异常)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .headers(headers -> headers
            .frameOptions(frame -> frame.deny())
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("default-src 'self'; script-src 'self'")
            )
        );
    return http.build();
}

问题分析:统一的 Content-Security-Policy 和 X-Frame-Options 应用到所有路径。/files/** 返回的图片如果需要通过 <img> 标签在其他页面加载,会被 CSP 限制;/health 端点返回 JSON 却携带 X-Frame-Options: DENY,无意义但可能干扰某些监控工具。

操作后配置(多链差异化头部)

@Configuration
@EnableWebSecurity
public class PathBasedHeadersConfig {

    // 链1:Web 管理页面(完整安全头部)
    @Bean
    @Order(1)
    public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/web/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ADMIN"))
            .formLogin(Customizer.withDefaults())
            .headers(headers -> headers
                .frameOptions(frame -> frame.sameOrigin())
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives(
                        "default-src 'self'; " +
                        "script-src 'self'; " +
                        "style-src 'self' 'unsafe-inline'; " +
                        "img-src 'self' data:; " +
                        "frame-ancestors 'self';"
                    )
                )
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000)
                )
                .referrerPolicy(referrer -> referrer
                    .policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
                )
                // 保留默认 X-Content-Type-Options, Cache-Control
            );
        return http.build();
    }

    // 链2:文件服务(放宽 CSP,保留 HSTS)
    @Bean
    @Order(2)
    public SecurityFilterChain fileSecurity(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/files/**")
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/files/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .headers(headers -> headers
                // 文件不需要 frame 控制(不是 HTML 页面)
                .frameOptions(frame -> frame.disable())
                // 文件服务放宽 CSP:允许图片和文件下载
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("default-src 'none'; media-src 'self'")
                )
                // 保留 HSTS
                .httpStrictTransportSecurity(hsts -> hsts
                    .maxAgeInSeconds(31536000)
                )
                // 文件可以缓存(与默认 Cache-Control 相反)
                .cacheControl(cache -> cache.disable())
            );
        return http.build();
    }

    // 链3:健康检查(最小头部)
    @Bean
    @Order(3)
    public SecurityFilterChain healthSecurity(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/health")
            .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
            .headers(headers -> headers
                // 仅保留 HSTS,其余禁用
                .frameOptions(frame -> frame.disable())
                .contentSecurityPolicy(csp -> csp.disable())
                .cacheControl(cache -> cache.disable())
                .xssProtection(xss -> xss.disable())
                .contentTypeOptions(options -> options.disable())
                .httpStrictTransportSecurity(hsts -> hsts
                    .maxAgeInSeconds(31536000)
                )
            );
        return http.build();
    }

    @Bean
    @Order(4)
    public SecurityFilterChain defaultSecurity(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
            .headers(Customizer.withDefaults());  // 默认头部
        return http.build();
    }
}

测试验证与结果分析

请求路径X-Frame-OptionsCSPCache-ControlHSTS说明
/web/dashboardSAMEORIGIN严格策略no-cache有完整安全头部
/files/public/logo.png无default-src 'none'无(可缓存)有放宽缓存和图片策略
/health无无无有最小头部,JSON 响应无干扰
/other/pageDENY(默认)默认no-cache无(默认配置)兜底链

易错场景:HeaderWriterFilter 位置导致头部被覆盖或丢失

问题现象

开发者自定义了一个 Filter 并在 HeaderWriterFilter 之后添加,该过滤器修改了响应状态码或调用了 response.reset()。导致 HeaderWriterFilter 写入的安全头部丢失。

错误配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .addFilterAfter(new CustomResponseFilter(), HeaderWriterFilter.class);
    return http.build();
}

public class CustomResponseFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain) 
                                    throws ServletException, IOException {
        chain.doFilter(request, response);
        
        // 错误:在 HeaderWriterFilter 之后调用 response.reset()
        // 这清除了所有已写入的响应头和响应体!
        if (response.getStatus() == 500) {
            response.reset();
            response.setStatus(500);
            response.getWriter().write("Internal Server Error");
        }
    }
}

问题分析:HeaderWriterFilter 的 HeaderWriterResponse 包装器在 response.getOutputStream().flush() 或 response.getWriter().flush() 时触发头部写入。如果后续过滤器调用 response.reset(),Servlet 容器会清除所有已设置的响应头(包括 HeaderWriterFilter 写入的安全头部),导致安全头丢失。

正确做法

  1. 不要在自定义过滤器中调用 response.reset(),除非确实需要清空所有内容,且意识到这会丢失安全头部。
  2. 如果必须在响应提交后修改状态,使用 @ControllerAdvice 处理异常,而不是在过滤器中重置响应。
  3. 自定义过滤器如果需要包装响应,应在 HeaderWriterFilter 之前执行,避免覆盖其写入的头部。
// 正确的异常处理:使用 @ControllerAdvice,在 HeaderWriterFilter 之后、Controller 之前
@ControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception ex) {
        // 返回 ResponseEntity,HeaderWriterFilter 已在之前执行,头部保留
        return ResponseEntity.status(500)
            .body(new ErrorResponse("Internal Server Error", ex.getMessage()));
    }
}

// 如果必须在过滤器中处理,避免 reset()
public class SafeResponseFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain) 
                                    throws ServletException, IOException {
        // 包装响应以捕获状态码,但不 reset()
        StatusCaptureResponseWrapper wrapper = 
            new StatusCaptureResponseWrapper(response);
        chain.doFilter(request, wrapper);
        
        // 基于状态码做日志记录,但不修改响应
        if (wrapper.getStatus() == 500) {
            log.error("请求 {} 返回 500", request.getRequestURI());
        }
        // 响应头和响应体保持原样,安全头部不受影响
    }
}

面试考点:HeaderWriterFilter 使用响应包装器(HeaderWriterResponse)延迟写入头部,这种设计的目的是什么?如果某个后续过滤器调用了 response.reset(),会导致什么问题? 答:HeaderWriterFilter 使用 HttpServletResponseWrapper 包装响应的目的是延迟写入头部到响应真正提交时。因为响应头必须在响应体写入之前设置,如果 HeaderWriterFilter 在 doFilter 开始时立即写入头部,后续过滤器可能通过 response.reset() 或 response.setHeader() 覆盖或清除这些头部。延迟写入确保头部在最终响应提交前设置,且不会被后续过滤器的常规操作覆盖。但如果后续过滤器显式调用 response.reset(),Servlet 规范要求清除所有已设置的响应头(包括状态码、头部和缓冲区),这会导致 HeaderWriterFilter 写入的安全头部全部丢失。因此应避免在过滤器链中调用 response.reset(),或确保此类操作在 HeaderWriterFilter 之前执行。

上一页
CorsFilter
下一页
AnonymousAuthenticationFilter