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
  1. max-age:设置在浏览器收到这个请求后的多少秒的时间内,凡是访问这个域名下的请求都使用 HTTPS 请求。

  2. 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 证书供我们测试使用,具体步骤如下:

  1. 确保本地 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所示。

    image 2024 04 14 16 11 20 403
    Figure 1. 图9-7HTTPS证书生成过程
  2. 接下来将生成的 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:就是在密钥生成过程中输入的口令。

  3. 配置完成后,启动项目。浏览器中输入 https://localhost:8080/login 进行访问,由于这个 HTTPS 证书是我们自己生成的,并不被浏览器认可,所以在访问的时候会有安全提示,大家单击继续访问即可,如图9-8所示。

image 2024 04 14 16 40 54 546
Figure 2. 图9-8 选择继续前往localhost即可

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

image 2024 04 14 16 41 54 880
Figure 3. 图9-9 响应头中的 Strict-Transport-Security 字段

如果需要对 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)时,浏览器将停止加载页面,该响应头有四种不同的取值:

  1. 0 表示禁止 XSS 过滤。

  2. 1 表示启用 XSS 过滤(通常浏览器是默认的)。如果检测到跨站本攻击,浏器将清除页面(删除不安全的部分)。

  3. 1;mode=block 表示启用 XSS 过滤。如果检测到攻击,浏览器将不会清除页面,而是阻止页面加载。

  4. 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所示。

image 2024 04 14 17 42 15 591
Figure 4. 图9-10 失去了 CSS 样式的登录页面

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

image 2024 04 14 17 43 10 807
Figure 5. 图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所示

image 2024 04 14 17 46 10 192
Figure 6. 图9-12 启用 read-only 模式的 CSP 响应头

此时,浏览器还是会去加载那些被禁止的外部资源,同时会将违规行为发送到 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所示。

image 2024 04 14 18 11 05 870
Figure 7. 图9-13 注销登录时的响应头