Spring Security 7 响应头配置完全指南

Spring Security 响应头配置完全指南

前言

在 Web 应用安全中,HTTP 响应头是防御型安全的第一道防线。Spring Security 提供了开箱即用的安全响应头功能,但默认配置往往不能满足所有场景需求。本文将详细探讨 Spring Security 响应头的配置方法,以及如何解决实际项目中遇到的响应头冲突问题。

一、问题回顾:X-Frame-Options 响应头冲突

1.1 问题现象

我们在项目中添加了 xFrameOptionsInterceptor 拦截器,在 Controller 的 postHandle 方法中设置:

java 复制代码
response.setHeader("X-Frame-Options", "SAMEORIGIN");

但实际测试发现,响应头仍然是 DENY,自定义的 SAMEORIGIN 配置没有生效。

1.2 根因分析

问题的根源在于执行顺序

组件 类型 执行时机 设置的值
Spring Security HeaderWriterFilter Security Filter Spring Security 过滤器链(第5个) DENY(默认值)
xFrameOptionsInterceptor.postHandle HandlerInterceptor Spring MVC Controller 之后 SAMEORIGIN

Spring Security 6+ 默认行为

java 复制代码
// Spring Security 默认相当于:
headers(headers -> headers
    .frameOptions(frame -> frame.deny())  // 默认 DENY
)

由于 HeaderWriterFilter 在 Spring MVC 拦截器之前执行,它设置的 DENY 已经写入响应,后续拦截器的修改无法覆盖。

1.3 解决方案

修改配置(推荐):

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        // 其他配置...
        .headers(headers -> headers
            .frameOptions(frame -> frame.sameOrigin())
        );

    return http.build();
}

移除拦截器(如果使用了 Spring Security 配置,就不需要 MVC 拦截器了)。

二、Spring Security 响应头框架

Spring Security 的响应头配置通过 HttpSecurity.headers() 方法实现,支持两类响应头:

  1. 安全响应头(Security Headers):防范常见 Web 攻击
  2. 静态资源响应头(Static Resource Headers):针对静态资源的缓存控制

2.1 安全响应头详解

1. X-Frame-Options - 点击劫持防护
java 复制代码
.headers(headers -> headers
    // 禁用(不推荐,生产环境应启用)
    .frameOptions(frame -> frame.disable())

    // DENY:禁止所有 iframe 加载
    .frameOptions(frame -> frame.deny())

    // SAMEORIGIN:仅允许同源 iframe 加载(最常用)
    .frameOptions(frame -> frame.sameOrigin())

    // ALLOW-FROM:允许指定来源的 iframe(已废弃,使用 CSP frame-ancestors)
    .frameOptions(frame -> frame.allowFrom(origins -> "https://example.com"))
)

推荐配置

java 复制代码
.frameOptions(frame -> frame.sameOrigin())
2. X-Content-Type-Options - MIME 类型嗅探防护
java 复制代码
.headers(headers -> headers
    // 禁用 nosniff(不推荐)
    .contentTypeOptions(contentType -> contentType.disable())

    // 启用 nosniff,禁止 MIME 类型嗅探
    .contentTypeOptions(AbstractHttpConfigurer::disable)  // 默认启用
)

效果:响应头添加 X-Content-Type-Options: nosniff,浏览器不会执行非声明类型的资源。

3. Strict-Transport-Security (HSTS) - 强制 HTTPS
java 复制代码
.headers(headers -> headers
    // 自定义 HSTS 配置
    .hsts(hsts -> hsts
        .includeSubDomains(true)           // 包含子域名
        .maxAgeInSeconds(31536000)         // 有效期 1 年
        .preload(true)                      // 允许 HSTS Preload 列表
    )

    // 禁用 HSTS(仅在开发环境)
    .hsts(hsts -> hsts.disable())
)

效果:响应头添加 Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

生产环境推荐配置

java 复制代码
.hsts(hsts -> hsts
    .includeSubDomains(true)
    .maxAgeInSeconds(31536000)  // 1 年
    .requestTimespanEnforcement(true)  // 请求时强制检查
)
4. X-XSS-Protection - XSS 过滤器(已废弃)
java 复制代码
.headers(headers -> headers
    // 启用 XSS 过滤保护
    .xssProtection(xss -> xss
        .headerValue("1; mode=block")  // 阻塞页面而非过滤
    )

    // 禁用
    .xssProtection(xss -> xss.disable())
)

注意:现代浏览器已废弃此头部,建议使用 CSP 替代。Spring Security 7 默认禁用。

5. Content-Security-Policy (CSP) - 内容安全策略
java 复制代码
.headers(headers -> headers
    // 基础配置
    .contentSecurityPolicy(csp -> csp
        .policyDirectives("default-src 'self'")
    )

    // 详细配置示例
    .contentSecurityPolicy(csp -> csp
        .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'")
    )

    // 仅报告模式(不阻断,仅报告)
    .contentSecurityPolicy(csp -> csp
        .policyDirectives("default-src 'self'; report-uri /csp-violation-report")
        .reportOnly(true)
    )

    // 禁用 CSP
    .contentSecurityPolicy(csp -> csp.disable())
)

常用指令

指令 说明 示例值
default-src 默认来源 'self'
script-src JavaScript 来源 'self' 'unsafe-inline' https://cdn.example.com
style-src CSS 来源 'self' 'unsafe-inline'
img-src 图片来源 'self' data: https: blob:
connect-src AJAX/WebSocket 'self' https://api.example.com
font-src 字体来源 'self' data: https://fonts.gstatic.com
frame-ancestors iframe 嵌入源 'self'(替代 X-Frame-Options)
base-uri base 标签限制 'self'
form-action 表单提交限制 'self'

生产环境推荐 CSP

java 复制代码
.contentSecurityPolicy(csp -> csp
    .policyDirectives(
        "default-src 'self'; " +
        "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
        "style-src 'self' 'unsafe-inline'; " +
        "img-src 'self' data: https:; " +
        "font-src 'self' data:; " +
        "connect-src 'self'; " +
        "frame-ancestors 'self'"
    )
)
6. Referrer-Policy - 引用来源策略
java 复制代码
.headers(headers -> headers
    .referrerPolicy(referrer -> referrer
        .policy(ReferrerPolicy.SAME_ORIGIN)  // 同源
        // 可选值:
        // - no-referrer
        // - no-referrer-when-downgrade(默认值)
        // - origin
        // - origin-when-cross-origin
        // - same-origin
        // - strict-origin
        // - strict-origin-when-cross-origin
        // - unsafe-url
    )

    .referrerPolicy(referrer -> referrer.disable())
)

推荐配置(平衡隐私和功能):

java 复制代码
.referrerPolicy(referrer -> referrer
    .policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
7. Permissions-Policy - 浏览器功能策略
java 复制代码
.headers(headers -> headers
    .permissionsPolicy(permissions -> permissions
        .policy(
            "geolocation=(), " +
            "microphone=(), " +
            "camera=(), " +
            "payment=(), " +
            "usb=()"
        )
    )
)

效果:Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()

常用功能限制

功能 说明
geolocation 地理位置
microphone 麦克风
camera 摄像头
payment 支付 API
fullscreen 全屏模式
gyroscope 陀螺仪
accelerometer 加速度计
8. Cache-Control - 缓存控制
java 复制代码
.headers(headers -> headers
    // 禁用缓存头
    .cacheControl(cache -> cache.disable())

    // 自定义缓存控制
    .cacheControl(cache -> cache
        .headerWriter(new CustomCacheControlHeaderWriter(
            CacheControl.maxAge(7, TimeUnit.DAYS).cachePublic()
        ))
    )
)
9. Cache-Control(静态资源优化)
java 复制代码
// 针对静态资源的缓存配置
.headers(headers -> headers
    .cacheControl(cache -> cache
        .headerWriter(new StaticResourceCacheControl())
    )
)

Spring Security 默认静态资源缓存头

  • CSS/JS:max-age=31536000(1年)
  • HTML:no-cache, no-store, must-revalidate

2.2 一键配置所有安全头

Spring Security 提供了便捷的默认配置:

java 复制代码
.headers(headers -> headers
    .httpStrictTransportSecurity(hsts -> hsts
        .includeSubDomains(true)
        .maxAgeInSeconds(31536000)
    )
    .xssProtection(xss -> xss.block(true).headerValue("1; mode=block"))
    .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
    .frameOptions(frame -> frame.sameOrigin())
    .contentTypeOptions(contentType -> {})  // 默认启用
    .referrerPolicy(referrer -> referrer.policy(ReferrerPolicy.SAME_ORIGIN))
    .permissionsPolicy(permissions -> permissions.policy("geolocation=()"))
)

三、实战:完整的响应头配置

3.1 开发环境配置

java 复制代码
@Configuration
@EnableWebSecurity
public class DevSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                // 禁用 HSTS(方便 HTTP 调试)
                .hsts(hsts -> hsts.disable())
                // 禁用 X-Frame-Options(iframe 调试)
                .frameOptions(frame -> frame.disable())
                // 禁用 CSP(避免 JS 被拦截)
                .contentSecurityPolicy(csp -> csp.disable())
                // 其他保持默认
                .contentTypeOptions(AbstractHttpConfigurer::disable)
                .xssProtection(xss -> xss.disable())
            );

        return http.build();
    }
}

3.2 生产环境配置

java 复制代码
@Configuration
@EnableWebSecurity
public class ProdSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                // HSTS - 强制 HTTPS
                .hsts(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000)  // 1 年
                    .preload(true)
                )
                // CSP - 内容安全策略
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives(
                        "default-src 'self'; " +
                        "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.example.com; " +
                        "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
                        "img-src 'self' data: https:; " +
                        "font-src 'self' data: https://fonts.gstatic.com; " +
                        "connect-src 'self' https://api.example.com; " +
                        "frame-ancestors 'self'; " +
                        "base-uri 'self'; " +
                        "form-action 'self'"
                    )
                )
                // X-Frame-Options - 点击劫持防护
                .frameOptions(frame -> frame.sameOrigin())
                // X-Content-Type-Options - MIME 嗅探防护
                .contentTypeOptions(AbstractHttpConfigurer::disable)
                // Referrer-Policy - 引用来源策略
                .referrerPolicy(referrer -> referrer
                    .policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
                )
                // Permissions-Policy - 浏览器功能限制
                .permissionsPolicy(permissions -> permissions
                    .policy(
                        "geolocation=(), " +
                        "microphone=(), " +
                        "camera=(), " +
                        "payment=(), " +
                        "usb=()"
                    )
                )
                // X-XSS-Protection(可选,已有 CSP 可禁用)
                .xssProtection(xss -> xss.disable())
            );

        return http.build();
    }
}

3.3 响应头验证

部署后使用 curl 验证:

bash 复制代码
# 检查所有安全响应头
curl -I https://your-domain.com/

# 预期输出:
# HTTP/2 200
# strict-transport-security: max-age=31536000; includeSubDomains; preload
# x-content-type-options: nosniff
# x-frame-options: SAMEORIGIN
# content-security-policy: default-src 'self'; ...
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: geolocation=(), microphone=(), ...

使用在线工具验证:

四、自定义响应头

4.1 添加自定义 HeaderWriter

java 复制代码
import org.springframework.security.web.header.HeaderWriter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class CustomHeaderWriter implements HeaderWriter {

    @Override
    public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
        response.setHeader("X-Custom-Header", "custom-value");
        response.setHeader("X-Request-ID", UUID.randomUUID().toString());
    }
}

// 配置使用
.headers(headers -> headers
    .addHeaderWriter(new CustomHeaderWriter())
    // 或者使用多个
    .addHeaderWriter(new HeaderWriter() {
        @Override
        public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
            response.setHeader("X-Version", "1.0.0");
        }
    })
)

4.2 条件响应头

java 复制代码
public class ConditionalHeaderWriter implements HeaderWriter {

    @Override
    public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
        // 仅对 API 请求添加自定义头
        if (request.getRequestURI().startsWith("/api/")) {
            response.setHeader("X-API-Version", "v1");
        }

        // 生产环境添加调试头
        if (!isProductionEnvironment()) {
            response.setHeader("X-Debug-Mode", "true");
        }
    }

    private boolean isProductionEnvironment() {
        // 根据配置判断
        return true;
    }
}

4.3 DelegatingHeaderWriter - 组合多个 Writer

java 复制代码
@Bean
public HeaderWriter combinedHeaderWriter() {
    List<HeaderWriter> writers = List.of(
        new XFrameOptionsHeaderWriter(Mode.SAMEORIGIN),
        new StaticResourceCacheControl(),
        new CustomHeaderWriter()
    );
    return new DelegatingHeaderWriter(writers);
}

五、最佳实践

5.1 响应头优先级

  1. Spring Security 配置 > MVC 拦截器:Spring Security 的 HeaderWriterFilter 先执行
  2. 后覆盖前 :同一个 Header,后设置的值会覆盖前面的值(但 Set-Cookie 是追加)

5.2 配置建议

环境 建议
开发环境 禁用 HSTS 和 X-Frame-Options,方便调试
测试环境 启用完整配置,验证 CSP 策略
生产环境 启用全部安全头,配置合理的 CSP

5.3 CSP 渐进式实施

  1. Report-Only 模式:先收集违规报告
  2. 逐步收紧策略:根据报告调整 CSP
  3. 完全强制执行:确认无问题后移除 report-only
java 复制代码
// 第一阶段:仅报告
.contentSecurityPolicy(csp -> csp
    .policyDirectives("default-src 'self'")
    .reportOnly(true)
)

// 第二阶段:完全启用
.contentSecurityPolicy(csp -> csp
    .policyDirectives("default-src 'self'; script-src 'self'")
    .reportOnly(false)
)

5.4 安全头与业务平衡

业务需求 推荐方案
第三方 JS 集成 使用 script-src 'unsafe-inline' 或 nonce 策略
内联样式 使用 style-src 'unsafe-inline' 或 CSS nonce
Iframe 嵌套 配置 frame-ancestors 白名单
文件上传 配置 form-action 限制提交目标

六、常见问题

Q1: X-Frame-Options 和 CSP frame-ancestors 同时配置会怎样?

两者都会生效,但 CSP frame-ancestors 是更现代的标准。建议:

  • 主要使用 CSP frame-ancestors
  • X-Frame-Options 作为兼容性备份
java 复制代码
.headers(headers -> headers
    .frameOptions(frame -> frame.sameOrigin())  // 备用
    .contentSecurityPolicy(csp -> csp
        .policyDirectives("frame-ancestors 'self'")  // 主要
    )
)

Q2: 如何动态修改 CSP?

可以通过 Controller 返回 Content-Security-Policy 头动态覆盖:

java 复制代码
@RestController
public class CspController {

    @GetMapping("/api/csp-report")
    public ResponseEntity<Void> cspReport(@RequestBody CspViolationReport report) {
        log.warn("CSP Violation: {}", report);
        return ResponseEntity.ok().build();
    }
}

Q3: Spring Security 默认启用哪些响应头?

Spring Security 7 默认启用(可通过 .headers().disable() 全部禁用):

响应头 默认值
X-Frame-Options DENY
X-Content-Type-Options nosniff
Strict-Transport-Security 未启用(需配置)
X-XSS-Protection 1; mode=block(已废弃)
Cache-Control(静态资源) max-age=31536000

Q4: 为什么 HSTS 不生效?

  1. 仅 HTTP 请求:HSTS 只对 HTTPS 响应生效
  2. 首次访问:首次请求时设置,后续请求才生效
  3. includeSubDomains:确保子域名也受保护

Q5: 如何测试 CSP?

javascript 复制代码
// 浏览器控制台会显示违规信息
console.log('CSP test');

// 收集违规报告
window.addEventListener('securitypolicyviolation', (e) => {
    console.log('CSP Violation:', e);
});

七、总结

Spring Security 的响应头配置是 Web 应用安全的重要组成:

  1. 理解执行顺序:Spring Security 过滤器优先于 MVC 拦截器
  2. 正确配置 :使用 http.headers() 而非 MVC 拦截器
  3. 渐进式部署:特别是 CSP,建议从 report-only 开始
  4. 持续验证:使用工具定期检查响应头配置

通过合理配置这些响应头,可以有效防范:

  • 点击劫持(X-Frame-Options)
  • MIME 嗅探攻击(X-Content-Type-Options)
  • XSS 攻击(CSP、X-XSS-Protection)
  • 中间人攻击(HSTS)
  • 信息泄露(Referrer-Policy)

生产环境推荐配置

java 复制代码
.headers(headers -> headers
    .hsts(hsts -> hsts.includeSubDomains(true).maxAgeInSeconds(31536000))
    .contentSecurityPolicy(csp -> csp.policyDirectives(
        "default-src 'self'; script-src 'self' 'unsafe-inline'; " +
        "style-src 'self' 'unsafe-inline'; img-src 'self' data:; " +
        "frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
    ))
    .frameOptions(frame -> frame.sameOrigin())
    .contentTypeOptions(AbstractHttpConfigurer::disable)
    .referrerPolicy(referrer -> referrer.policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
    .permissionsPolicy(permissions -> permissions.policy("geolocation=(), microphone=(), camera=()"))
)

参考文档

相关推荐
会算数的⑨1 小时前
Spring AI Alibaba学习(一)—— RAG
java·人工智能·后端·学习·spring·saa
bug-0071 小时前
springboot 自定义消息处理
java·spring boot·后端
我真的是大笨蛋2 小时前
MySQL临时表深度解析
java·数据库·sql·mysql·缓存·性能优化
九皇叔叔2 小时前
【02】微服务系列 之 初始化工程
java·数据库·微服务
季明洵2 小时前
两数之和、四数相加II、三数之和、四数之和
java·数据结构·算法·leetcode·蓝桥杯·哈希算法
人道领域2 小时前
javaWeb从入门到进阶(SpringBoot基础案例3)
java·spring boot·后端
西门吹-禅2 小时前
.gradle迁移d盘
java
士别三日&&当刮目相看2 小时前
Spring两大核心思想:IoC和AOP
java·spring
轩情吖2 小时前
数据结构-并查集
开发语言·数据结构·c++·后端··并查集