返回介绍

9.2 HTTP 响应头处理

发布于 2025-04-26 13:16:46 字数 18020 浏览 0 评论 0 收藏

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> {
       @Override
       public void configure(H http) {
           HeaderWriterFilter headersFilter = createHeaderWriterFilter();
           http.addFilter(headersFilter);
       }
       private HeaderWriterFilter createHeaderWriterFilter() {
           List<HeaderWriter> writers = getHeaderWriters();
           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 响应头。

了解到这响应头的来源之后,接下来我们来对其逐个进行分析。

9.2.1 缓存控制

和缓存控制相关的响应头一共有三个:

    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 三个缓存相关的响应头了。

9.2.2 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 响应头。

9.2.3 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 所示。

图 9-7 HTTPS 证书生成过程

(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 所示。

图 9-8 选择继续前往 localhost 即可

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

图 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() 方法移除该响应头。

9.2.4 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 页面的一些功能性按钮上。

9.2.5 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 默认会自动配置的响应头。还有其他一些安全相关的响应头,需要我们手动配置,一起来看一下。

9.2.6 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 所示。

图 9-10 失去了 CSS 样式的登录页面

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

图 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 所示。

图 9-12 启用了 read-only 模式的 CSP 响应头

此时,浏览器还是会去加载那些被禁止的外部资源,同时会将违规行为发送到 http://localhost:8081/report 地址,开发者收到违规行为报告后可以自行处理。

9.2.7 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(移除参数信息之后)作为引用地址。

9.2.8 Feature-Policy

Feature-Policy 响应头提供了一种可以在本页面或包含的 iframe 上启用或禁止浏览器特性的机制(移动端开发使用较多)。举一个简单例子,如果想要禁用震动和定位 API,那么可以在响应头中添加如下内容:

    Feature-Policy: vibrate 'none'; geolocation 'none'

Spring Security 中配置如下:

    Feature-Policy: vibrate 'none'; geolocation 'none'
    @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'");
       }
    }

该功能使用较少,这里不做过多介绍。

9.2.9 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 HeaderWriterLogoutHandler(
    new ClearSiteDataHeaderWriter(ClearSiteDataHeaderWriter.Directive.ALL)))
                   .and()
                   .csrf().disable();
       }
    }

在注销登录的处理器中,设置了清除浏览器所有和当前网站相关的数据。配置完成后,当浏览器发起注销登录请求时,响应头中就会有 Clear-Site-Data,如图 9-13 所示。

图 9-13 注销登录时的响应头

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。