HTTP响应头处理
HTTP 响应头中的许多属性都可以用来提高 Web 安全。本节我们来看一下 Spring Security 中提供显式支持的一些 HTTP 响应头。
Spring Security 默认情况下,显式支持的 HTTP 响应头主要有如下几种:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-options: DENY
X-xss-Protection: 1; mode=block
这里一共有七个响应头,前三个都是与缓存相关的,因此一共可以分为五大类。
这些响应头都是在 HeaderWriterFilter 中添加的,默认情况下,该过滤器就会添加到 Spring Security 过滤器链中,HeaderWriterFilter 是通过 HeadersConfigurer 进行配置的,我们来看一下 HeadersConfigurer 中几个关键的方法:
public class HeadersConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractHttpConfigurer<HeadersConfigurer<H>, H> {
public void configure(H http) {
HeaderWriterFilter headersFilter = createHeaderWriterFilter();
http.addFilter(headersFilter);
}
private HeaderWriterFilter createHeaderWriterFilter() {
List<HeaderWriter> writers = getHeaderWriters();
if (writers.isEmpty()) {
throw new IllegalStateException(
"Headers security is enabled, but no headers will be added. Either add headers or disable headers security");
}
HeaderWriterFilter headersFilter = new HeaderWriterFilter(writers);
headersFilter = postProcess(headersFilter);
return headersFilter;
}
private List<HeaderWriter> getHeaderWriters() {
List<HeaderWriter> writers = new ArrayList<>();
addIfNotNull(writers, contentTypeOptions.writer);
addIfNotNull(writers, xssProtection.writer);
addIfNotNull(writers, cacheControl.writer);
addIfNotNull(writers, hsts.writer);
addIfNotNull(writers, frameOptions.writer);
addIfNotNull(writers, hpkp.writer);
addIfNotNull(writers, contentSecurityPolicy.writer);
addIfNotNull(writers, referrerPolicy.writer);
addIfNotNull(writers, featurePolicy.writer);
writers.addAll(headerWriters);
return writers;
}
private <T> void addIfNotNull(List<T> values, T value) {
if (value != null) {
values.add(value);
}
}
}
可以看到,这里在 configure 方法中创建了 HeaderWriterFilter 过滤器,在过滤器创建时,通过 getHeaderWriters 方法获取到所有需要添加的响应头传入过滤器中。getHeaderWriters 方法执行时,只会添加不为 null 的实例,默认情况下,只有前五个不为 null,其中:
-
contentTypeOptions.writer:负责处理 X-Content-Type-Options 响应头。
-
xssProtection.writer:负责处理 X-XSS-Protection 响应头。
-
cacheControl.writer:负责处理 Cache-Control、Pragma 以及Expires 响应头。
-
hsts.writer:负责处理 Strict-Transport-Security 响应头。
-
frameOptions.writer:负责处理 X-Frame-Options 响应头。
了解到这响应头的来源之后,接下来我们来对其逐个进行分析。
缓存控制
和缓存控制相关的响应头一共有三个:
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
可能有的读者对这几个响应头还不太熟悉,这稍微解释一下。
Cache-Control
Cache-Control 是 HTTP/1.1 中引入的缓存字段,无论是请求头还是响应头都支持该字段。其中 no-store 表示不做任何缓存,每次请求都会从服务端完整地下载内容。no-cache 则表示缓存但是需要重新验证,这种情况下,数据虽然缓存在客户端,但是当需要使用该数据时,还是会向服务端发送请求,服务端则验证请求中所描述的缓存是否过期,如果没有过期,则返回 304,客户端使用缓存:如果已经过期,则返回最新数据。max-age 则表示缓存的有效期,这个有效期并非一个时间戳,而是一个秒数,指从请求发起后多少秒内缓存有效。 must-revalidate 表示当缓存在使用一个陈旧的资源时,必须先验证它的状态,已过期的将不被使用。
Pragma
Pragma 是 HTTP/1.0 中定义的响应头,作用类似于 Cache-Control:no-cache,但是并不能代替 Cache-Control,该字段主要用来兼容 HTTP/1.0 的客户端。
Expires
Expires 响应头指定了一个日期,即在指定日期之后,缓存过期。如果日期值为0 的话,表示缓存已经过期。
从上面的解释可以看到,Spring Security 默认就是不做任何缓存。但是需要注意,这个是针对经过 Spring Security 过滤器的请求,如果请求本身都没经过 Spring Security 过滤器,那么该缓存的还是会缓存的。例如如下代码:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello.html");
}
}
当访问 /hello.html 时,请求就不会经过 Spring Security 过滤器,所以该资源还是会缓存的(回顾本书4.5节)。
如果请求经过 Spring Security 过滤器,同时开发者又希望开启缓存功能,那么可以关闭 Spring Security 中关于缓存的默认配置,代码如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.headers()
.cacheControl()
.disable()
.and()
.formLogin()
.and()
.csrf().disable();
}
}
调用 .cacheControl().disable() 方法之后,Spring Security 就不会配置 Cache-Control、Pragma 以及 Expires 三个缓存相关的响应头了。
X-Content-Type-Options
要理解 X-Content-Type-Options 响应头,得先了解 MIME 嗅探。
一般来说,浏览器通过响应头 Content-Type 来确定响应报文类型,但是在早期浏览器中,为了提高用户体验,并不会严格根据 Content-Type 的值来解析响应报文,当 Content-Type 的值缺失,或者浏览器认为服务端给出了错误的 Content-Type 值,此时就会对响应报文进行自我解析,即自动判断报文类型然后进行解析,在这人过程中就有可能触发 XSS 攻击。
X-Content-Type-Options 响应头相当于一个提示标志,被服务器用来提示客户端一定要遵循在 Content-Type 中对 MIME 类型的设定,而不能对其进行修改。这就禁用了客户端的 MIME 类型嗅探行为,换言之,就是服务端告诉客户端其对于 MIME 类型的设置没有任何问题。
配置后响应头如下:
X-Content-Type-Options: nosniff
如果开发者不想禁用 MIME 嗅探,可以通过如下方式从响应头中移除 X-Content-Type-options。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.headers()
.contentTypeOptions()
.disable()
.and()
.formLogin()
.and()
.csrf().disable();
}
}
调用 .contentTypeOptions().disable() 方法即可移除 X-Content-Type-Options 响应头。
Strict-Transport-Security
Strict-Transport-Security 用来指定当前客户端只能通过 HTTPS 访问服务端,而不能通过 HTTP 访问。
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
-
max-age:设置在浏览器收到这个请求后的多少秒的时间内,凡是访问这个域名下的请求都使用 HTTPS 请求。
-
includeSubDomains:这个参数是可选的,如果被指定,表示第 1 条规则也适用于了域名。
这个响应头并非总是会添加,如果当前请求是 HTTPS 请求,这个请求头才会添加,否则该请求头就不会添加,具体实现逻辑在 HstsHeaderWriter#writeHeaders 方法中:
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (this.requestMatcher.matches(request)) {
if (!response.containsHeader(HSTS_HEADER_NAME)) {
response.setHeader(HSTS_HEADER_NAME, this.hstsHeaderValue);
}
}
}
可以看到,向 response 中添加响应头之前,会先调用 requestMatcher.matches 方法对当前请求进行判断,判断当前请求是否是 HTTPS 请求,如果是 HTTPS 请求,则添加该响应头,否则不添加。
为了看到该响应头的效果,我们可以使用 Java 自带的 keytool 工具来生成一个 HTTPS 证书供我们测试使用,具体步骤如下:
-
确保本地 Java 已经安装好,环境变量也已经配置好,我们首先在命令行执行如下命令生成 HTTPS 证书:
keytool -genkey -alias tomcathttps -keyalg RSA -keysize 2048 -keystore javaboy.p12 -validity 365
命令含义如下:
-
genkey:表示要创建一个新的密钥
-
alias:表示 keystore 的别名。
-
keyalg:表示使用的加密算法是 RSA,一种非对称加密算法。
-
keysize:表示密钥的长度。
-
keystore:表示生成的密钥存放位置。
-
validity:表示密钥的有效时间,单位为天。
具体生成过程如图9-7所示。
Figure 1. 图9-7HTTPS证书生成过程 -
-
接下来将生成的 javaboy.p12 证书复制到 Spring Boot 项目的 resources 目录下,并在 application.properties 中添加如下配置:
server.ssl.key-store=classpath:javaboy.p12 server.ssl.key-alias=tomcathttps server.ssl.key-store-password=111111
-
key-store:表示密钥文件位置。
-
key-alias:表示密钥别名。
-
key-store-password:就是在密钥生成过程中输入的口令。
-
-
配置完成后,启动项目。浏览器中输入 https://localhost:8080/login 进行访问,由于这个 HTTPS 证书是我们自己生成的,并不被浏览器认可,所以在访问的时候会有安全提示,大家单击继续访问即可,如图9-8所示。

请求成功后,查看响应头,发现已经有了 Strict-Transport-Security 字段,如图9-9所示。

如果需要对 Strict-Transport-Security 的值进行具体配置,例如关闭 includeSubDomains 属性并重新设置 max-age,方式如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.httpStrictTransportSecurity()
.includeSubDomains(false)
.maxAgeInSeconds(3600);
}
}
当然也可以直接调用 .disable() 方法移除该响应头。
X-Frame-Options
X-Frame-Options 响应头用来告诉浏览器是否允许一个页面在 <frame>、<iframe>、<embed> 或者 <object> 中展现,通过该响应头可以确保网站没有被嵌入到其他站点里面,进而避免发生单击劫持。
X-Frame-Options 响应头有三种不同的取值:
-
deny:表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
-
sameorigin:表示该页面可以在相同域名页面的 frame 中展示
-
allow-from uri:表示该页面可以在指定来源的 frame 中展示。
Spring Security 中默认取值是 deny,代码如下:
X-Frame-Options: DENY
如果项目需要,开发者也可以对此进行修改,例如将 deny 改为 sameorigin,方式如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.frameOptions()
.sameOrigin();
}
}
当然也可以直接调用 .disable() 方法移除该响应头。
什么是单击劫持
单击劫持是一种视觉上的欺骗手段。攻击者将被劫持的网页放在一个 iframe 标签中,设置该 iframe 标签透明不可见,然后将 iframe 标签覆盖在另一个网页上,最后诱使用户在该网页上进行操作,通过调整计 iframe 页面的位置,可以诱使用户恰好单击在 iframe 页面的一些功能性按钮上。 |
X-XSS-Protection
X-XSS-Protection 响应头告诉浏览器,当检测到跨站脚本攻击(XSS)时,浏览器将停止加载页面,该响应头有四种不同的取值:
-
0 表示禁止 XSS 过滤。
-
1 表示启用 XSS 过滤(通常浏览器是默认的)。如果检测到跨站本攻击,浏器将清除页面(删除不安全的部分)。
-
1;mode=block 表示启用 XSS 过滤。如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。
-
1;report=<reporting-URI> 表示启用 XSS 过滤。如果检测到跨站脚本攻击,浏览器将清除页面,并使用 CSP report-uri 指令的功能发送违规报告(Chrome 支持)。
Spring Security 中设置的 X-XSS-Protection 响应头如下:
X-XSS-Protection: 1; mode=block
当然开发者也可以对此进行配置,例如想去除 mode=block 部分,方式如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.xssProtection()
.block(false);
}
}
当然也可以直接调用 .disable() 方法移除该响应头。
什么是xss攻击?
跨站脚本攻击(Cross-Site Scripting,XSS)是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的 JavaScript 代码,而浏览器无法区分出这是恶意的 JavaScript 代码还是正常的 JavaScript 代码。当被攻击者登录网站时,就会自动运行这些恶意代码,攻击者可以利用这些恶意代码去窃取 Cookie 信息、监听用户行为以及修改 DOM 结构。 |
前面介绍这些响应头是 Spring Security 默认会自动配置的响应头。还有其他一些安全相关的响应头,需要我们手动配置,一起来看一下。
Content-Security-Policy
内容安全策略(Content Security Policy,CSP)是一个额外的安全层,用于检测并削弱某些特定类型的攻击,例如跨站脚本(XSS)和数据注入攻击等。
CSP 相当于通过一个白名单明确告诉客户端,那些外部资源可以加载和执行。举一个简单例子:
Content-Security-Policy: default-src 'self';script-src 'self';
object-src 'none'; style-src cdn.javaboy.org; img-src *; child-src https:
这个响应头含义如下:
-
default-src 'self':默认情况下所有资源只能从当前域中加载。接下来细化的配置会覆盖 default-src,没有细化的选项则使用 default-src。
-
script-src 'self':表示脚本文件只能从当前域名加载。
-
object-src 'none':表示 object 标签不加载任何资源。
-
style-src cdn.javaboy.org:表示只加载来自 cdn.javaboy.org 的样式表。
-
img-src *:表示可以从任意地址加载图片。
-
child-src https:表示必须使用 HTTPS 来加载 frame。
CSP 其他可选值,读者可以参考 https://www.w3.org/TR/CSP2 一文。
Spring Security 为 Content-Security-Policy 提供了配置方法,如果我们需要配置,则方式如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.contentSecurityPolicy("default-src 'self'; script-src 'self'; object-src 'none'; style-src cdn.javaboy.org; img-src *; child-src https:");
}
}
配置完成后,重启项目,此时默认的登录页面变了,如图9-10所示。

默认登录页面中加载了外部样式表,现在由于 CSP 限制,外部样式表加载失败,如图 9-11 所示。

CSP 还有一种报告模式 一一report-only。在此模式下,CSP 策略不是强制性的,如果出现违规行为,还是会继续加载租应的脚本或者样式表,但是会将违规行为报告给一个指定的 URI 地址。
配置方式如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.contentSecurityPolicy(contentSecurityPolicyConfig -> {
contentSecurityPolicyConfig.policyDirectives("default-src 'self'; script-src 'self'; object-src 'none';style-src cdn.javaboy.org; img-src *; child-src https:;report-uri http://localhost:8081/report");
contentSecurityPolicyConfig.reportOnly();
});
}
}
这段配置最终生成的响应头如图9-12所示

此时,浏览器还是会去加载那些被禁止的外部资源,同时会将违规行为发送到 http://localhost:8081/report 地址,开发者收到违规行为报告后可以自行处理。
Referrer-Policy
Referrer-Policy 描述了用户从哪里进入到当前网页。
浏览器默认的取值如下:
Referrer Policy: no-referrer-when-downgrade
这个表示如果是从 HTTPS 网址链接到 HTTP 网址,就不发送 Referer 字段,其他情况发送。开发者可以通过 Spring Security 中提供的方法对此进行修改,方式如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.referrerPolicy()
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.ORIGIN);
}
}
这个配置取值是 origin,表示总是发送源信息(源信息仅包含请求协议和域名,不包含其他路径信息,与之相对的是完整的 URL)。其他的取值还有:
-
no-referrer:表示从请求头中移除 Referer 字段。
-
same-origin:表示链接到同源地址时,发送文件源信息作为引用地址,否则不发送。
-
Strict-origin:表示从 HTTPS 链接到 HTTP 时不发送源信息,否则发送。
-
origin-when-cross-origin:表示对于同源请求会发送完整的 URL 作为引用地址,但是对于非同源请求,则只发送源信息。
-
strict-origin-when-cross-origin:表示对于同源的请求,会发送完整的 URL 作为引用地址;跨域时,如果是从 HTTPS 链接到 HTTP,则不发送 Referer 字段,否则发送文件的源信息。
-
unsafe-url:表示无论是同源请求还是非同源请求,都发送完整的 URL(移除参数信息之后)作为引用地址。
Feature-Policy
Feature-Policy 响应头提供了一种可以在本页面或包含的 iframe 上启用或禁止浏览器特性的机制(移动端开发使用较多)。举一简单例子,如果想要禁用震动和定位 API,那么可以在响应头中添加如下内容:
Feature-Policy: vibrate 'none'; geolocation 'none'
Spring Security 中配置如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable()
.headers()
.featurePolicy("vibrate 'none'; geolocation 'none'");
}
}
该功能使用较少,这里不做过多介绍。
Clear-Site-Data
Clear-Site-Data 一般用在注销登录响应头中,表示告诉浏览器清除当前网站相关的数据(cookie、cache、storage 等)。可以通过具体的参数指定想要清除的数据,如 cookies、cache、storage 等,也可以通过 “*” 表示清除所有数据。
Spring Security 中配置如下:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout()
.addLogoutHandler(new HeadlerWriterLogoutHandler(new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.ALL))).and().csrf().disable();
}
}
在注销登录的处理器中,设置了清除浏览器所有和当前网站相关的数据。配置完成后,当浏览器发起注销登录请求时,响应头中就会有 Clear-Site-Data,如图9-13所示。
