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() 方法实现,支持两类响应头:
- 安全响应头(Security Headers):防范常见 Web 攻击
- 静态资源响应头(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 响应头优先级
- Spring Security 配置 > MVC 拦截器:Spring Security 的 HeaderWriterFilter 先执行
- 后覆盖前 :同一个 Header,后设置的值会覆盖前面的值(但
Set-Cookie是追加)
5.2 配置建议
| 环境 | 建议 |
|---|---|
| 开发环境 | 禁用 HSTS 和 X-Frame-Options,方便调试 |
| 测试环境 | 启用完整配置,验证 CSP 策略 |
| 生产环境 | 启用全部安全头,配置合理的 CSP |
5.3 CSP 渐进式实施
- Report-Only 模式:先收集违规报告
- 逐步收紧策略:根据报告调整 CSP
- 完全强制执行:确认无问题后移除 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 不生效?
- 仅 HTTP 请求:HSTS 只对 HTTPS 响应生效
- 首次访问:首次请求时设置,后续请求才生效
- includeSubDomains:确保子域名也受保护
Q5: 如何测试 CSP?
javascript
// 浏览器控制台会显示违规信息
console.log('CSP test');
// 收集违规报告
window.addEventListener('securitypolicyviolation', (e) => {
console.log('CSP Violation:', e);
});
七、总结
Spring Security 的响应头配置是 Web 应用安全的重要组成:
- 理解执行顺序:Spring Security 过滤器优先于 MVC 拦截器
- 正确配置 :使用
http.headers()而非 MVC 拦截器 - 渐进式部署:特别是 CSP,建议从 report-only 开始
- 持续验证:使用工具定期检查响应头配置
通过合理配置这些响应头,可以有效防范:
- 点击劫持(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=()"))
)
参考文档: