Java Spring Boot 应用默认并没有全面启用安全响应头,这是一个常被忽视的攻击面。攻击者常利用缺失的 HTTP 头实施点击劫持、MIME 类型嗅探、XSS 等攻击。启用内置的漏洞防护头,本质上就是让 Spring Security 自动为所有 HTTP 响应注入一组浏览器识别的安全指令,无需改动业务代码,即可将应用的基础防御能力提升一个等级。

核心机制:Spring Security 的默认安全头配置

只要在项目中引入 spring-boot-starter-security 依赖,Spring Security 就会自动启用一套默认的安全头策略。这套策略包括防点击劫持的 X-Frame-Options、防 MIME 嗅探的 X-Content-Type-Options、以及 X-XSS-Protection 等。你不需要写一行配置代码,这些头就已经生效了。但问题在于,默认值并不一定适合所有场景,而且很多开发者因为自定义了安全配置,意外覆盖了这些默认头,导致防护失效。

第一步:确认依赖与自动配置

在 pom.xml 中加入以下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

启动应用后,打开浏览器开发者工具,查看任意一个接口的响应头。你会看到 X-Content-Type-Options: nosniff、X-Frame-Options: DENY、X-XSS-Protection: 1; mode=block 等头已经自动出现。这是 Spring Security 的 SecurityHeadersAutoConfiguration 和对应的过滤器在起作用。如果你没有看到这些头,通常是因为你自定义了 SecurityFilterChain,并且没有保留默认的头配置。

第二步:理解每个安全头的防护范围

X-Frame-Options 防止页面被嵌入到 iframe 中,从而杜绝点击劫持。默认值是 DENY,表示完全禁止被嵌套。如果你的应用需要被某些同源页面嵌入,可以改为 SAMEORIGIN。但要注意,SAMEORIGIN 仍可能被同源内的恶意页面利用,所以更推荐使用 Content-Security-Policy 的 frame-ancestors 指令来替代它。

X-Content-Type-Options 只有一个值 nosniff。它阻止浏览器对响应内容类型进行自动嗅探,强制浏览器严格按照服务器声明的 Content-Type 来解析资源。比如,一个被标记为 text/plain 的脚本文件就不会被浏览器当作 JavaScript 执行,这能有效防御某些类型的存储型 XSS。

X-XSS-Protection 是旧版浏览器内置的 XSS 过滤器开关。默认值 1; mode=block 表示启用过滤并在检测到攻击时阻止页面渲染。但在现代浏览器中,这个头的作用已经很小,甚至可能被利用来实施旁路攻击,所以很多安全实践建议将其设为 0。Spring Security 默认仍然启用它,主要是为了兼容旧环境。

Cache-Control 默认被设为 no-cache, no-store, max-age=0, must-revalidate,这对于需要严格保密的响应(如个人信息页)非常重要,可以防止敏感数据被缓存在浏览器或中间代理中。但静态资源通常需要缓存,这时就需要在配置中针对静态资源路径单独放开缓存策略。

Content-Security-Policy 是防御 XSS 和数据注入攻击的最强手段,但 Spring Security 默认并不启用它,因为 CSP 策略需要根据应用实际加载的资源来定制,没有通用的默认值。你需要显式配置它。

第三步:在自定义安全配置中保留并增强安全头

大多数项目都会自定义 SecurityFilterChain。如果你直接返回 http.build() 而没有调用 http.headers(),默认的头配置仍然会生效。但一旦你调用了 http.headers(),就必须注意不要覆盖掉默认值。推荐的做法是在默认配置基础上进行定制:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .headers(headers -> headers
            .frameOptions(frameOptions -> frameOptions.deny())
            .xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
            .contentTypeOptions(Customizer.withDefaults())
            .cacheControl(Customizer.withDefaults())
            .httpStrictTransportSecurity(hsts -> hsts
                .includeSubDomains(true)
                .maxAgeInSeconds(31536000)
            )
        )
        .authorizeHttpRequests(auth -> auth
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults());
    return http.build();
}

这段配置显式保留了所有默认安全头,并额外启用了 HSTS(HTTP Strict Transport Security)。HSTS 强制浏览器在指定时间内只能通过 HTTPS 访问你的域名,彻底杜绝 SSL 剥离攻击。maxAgeInSeconds 设成一年是常见做法,includeSubDomains 表示子域名也强制 HTTPS。如果你的应用还没有全站 HTTPS,暂时不要启用 HSTS,否则会导致用户无法访问。

第四步:添加 Content-Security-Policy 头

CSP 策略需要根据应用实际情况来编写。一个比较安全且兼容性好的起点是:

http.headers(headers -> headers
    .contentSecurityPolicy(csp -> csp
        .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';")
    )
);

这个策略的含义是:所有资源默认只能从同源加载;脚本和样式允许内联(如果应用使用了内联脚本或样式,暂时需要保留 unsafe-inline,但长期目标应该是移除外联并启用 nonce 或 hash);图片允许同源和 data URI;字体、连接、表单提交都限制同源;frame-ancestors 'none' 完全禁止被嵌入,这是比 X-Frame-Options 更强大的替代方案;base-uri 限制 base 标签只能指向同源,防止 base 劫持攻击。在生产环境中,你需要根据控制台报出的 CSP 违规报告逐步收紧策略,最终移除 unsafe-inline。

第五步:处理静态资源的安全头冲突

Spring Security 的安全头默认作用于所有请求,包括静态资源。但 Cache-Control: no-cache 会导致静态资源无法被浏览器缓存,严重影响性能。你需要为静态资源路径单独配置缓存策略:

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return (web) -> web.ignoring().requestMatchers("/static/", "/css/", "/js/", "/images/");
}

使用 WebSecurityCustomizer 忽略静态资源路径后,Spring Security 的过滤器链将完全不处理这些请求,安全头也不会被添加。然后你可以通过其他方式(如 Nginx 或 Tomcat 配置)为这些资源单独设置合理的 Cache-Control 头。如果你仍然希望为静态资源添加一部分安全头,但又不想影响缓存,可以保留 Security 处理,但通过自定义过滤器来覆盖 Cache-Control,这需要更精细的控制。

第六步:验证安全头是否生效

配置完成后,不要凭感觉判断。使用 curl 命令或浏览器开发者工具逐个检查响应头:

curl -I https://your-domain.com/api/endpoint

确认返回中包含你期望的所有安全头,并且值正确。特别要注意 HSTS 头是否只在 HTTPS 响应中出现,因为 Spring Security 默认会检测请求协议,如果请求是 HTTP,HSTS 头不会被写入,这是正确的行为,防止在 HTTP 环境下发布 HSTS 策略导致浏览器拒绝访问。

另外,可以使用一些在线安全头检测工具(如 securityheaders.com)来扫描你的应用,它会给出评分和详细的缺失项。目标不是追求满分,而是确保每个缺失项都是你有意为之,而不是疏漏。

常见陷阱:自定义过滤器导致头丢失

如果你在过滤器链中添加了自定义过滤器,并且在过滤器中直接操作了 HttpServletResponse 的输出流,可能会导致 Spring Security 的 HeaderWriterFilter 写入的头被覆盖或丢失。确保你的自定义过滤器在响应提交之前不要关闭输出流,或者将自定义过滤器的顺序安排在 HeaderWriterFilter 之后。Spring Security 的过滤器顺序是严格定义的,HeaderWriterFilter 的默认顺序在 SecurityProperties 中指定,通常不需要调整,但如果你手动添加了过滤器并指定了顺序,就需要留意这个细节。

进阶:使用 Referrer-Policy 和 Permissions-Policy 头

Spring Security 的默认配置不包含 Referrer-Policy 和 Permissions-Policy,但这两个头对隐私和功能限制很有价值。你可以通过自定义 HeaderWriter 来添加:

http.headers(headers -> headers
    .addHeaderWriter((request, response) -> {
        response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
        response.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
    })
);

Referrer-Policy 设为 strict-origin-when-cross-origin 表示同源请求发送完整 URL,跨域请求只发送源(协议+域名),且在降级协议(HTTPS 到 HTTP)时不发送任何 Referrer。这是一个兼顾隐私和功能性的推荐值。Permissions-Policy 用来禁用浏览器特性,比如禁止所有页面使用地理位置、麦克风和摄像头,除非你明确需要这些功能。

总结配置清单

一个生产级的安全头配置应该包含:X-Frame-Options 或 CSP frame-ancestors、X-Content-Type-Options、Cache-Control(对动态响应)、HSTS(全站 HTTPS 前提下)、Content-Security-Policy(逐步收紧)、Referrer-Policy、Permissions-Policy。X-XSS-Protection 可以保留默认或设为 0。所有这些头都可以通过 Spring Security 的 Java 配置集中管理,无需修改应用代码。关键是要理解每个头的作用和限制,根据业务需求做取舍,而不是盲目复制粘贴配置。定期使用自动化工具扫描你的应用,确保安全头始终处于预期状态,因为依赖升级或代码变更都可能意外改变响应头行为。