本节讨论 Spring Security 对响应添加各种安全报头的支持。 Spring Security 允许用户轻松地注入默认的安全报头,以帮助保护他们的应用程序。Spring Security 的默认包含以下报头: 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
有关这些报头中的每一个的详细信息,请参阅相应的章节: 虽然这些报头中的每一个都被认为是最佳实践,但是应当注意,并非所有客户端都使用这些报头,因此鼓励进行额外的测试。 你可以自定义特定的报头。例如,假设希望 HTTP 响应报头看起来像下面这样: Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block 具体地说,你希望所有默认报头具有以下自定义:
你可以通过以下 Java 配置轻松地实现这一点: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .frameOptions().sameOrigin() .httpStrictTransportSecurity().disable(); } } 或者,如果使用 Spring Security XML 配置,则可以使用以下内容: <http> <!-- ... --> <headers> <frame-options policy="SAMEORIGIN" /> <hsts disable="true"/> </headers> </http> 如果不希望添加默认值,并希望对应该使用的控件进行显式控制,则可以禁用默认值。下面提供了基于 Java 和 XML 的配置的示例: 如果使用 Spring Security 的 Java 配置,下面只会添加缓存控制。 @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() // do not use any default headers unless explicitly listed .defaultsDisabled() .cacheControl(); } } 下面的 XML 只会添加缓存控制。。 <http> <!-- ... --> <headers defaults-disabled="true"> <cache-control/> </headers> </http> 如果需要,你可以禁用下列 HTTP 安全响应报头的 Java 配置: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers().disable(); } } 如果需要,可以使用以下 XML 配置禁用所有 HTTP 安全响应报头: <http> <!-- ... --> <headers disabled="true" /> </http> 在过去,Spring Security 要求你为 web 应用程序提供自己的缓存控制。这在当时看来是合理的,但是浏览器缓存已经演变成包括用于安全连接的缓存。这意味着用户可以查看经过身份验证的页面、注销,然后恶意用户可以使用浏览器历史来查看缓存的页面。为了帮助减轻这个,Spring Security 增加了缓存控制支持,它将将下列报头插入到你的响应中。 Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 简单地添加没有子元素的 <headers> 元素将自动添加缓存控制和其他一些保护。但是,如果只希望缓存控制,可以使用 Spring Security 的 XML 密码空间使用 <cache-control> 元素和 headers@defaults-disabled 属性启用该特性。 <http> <!-- ... --> <headers defaults-disable="true"> <cache-control /> </headers> </http> 类似地,你可以在 Java 配置中仅启用缓存控制,如下所示: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .defaultsDisabled() .cacheControl(); } } 如果你真的想缓存特定的响应,你的应用程序可以有选择地调用 HttpServletResponse.setHeader(String,String) 来重写 Spring Security 报头设置。这有助于确保像 CSS、JavaScript 和图像这样的东西被正确地缓存。 当使用 Spring Web MVC 时,通常在配置内完成。例如,以下配置将确保为所有资源设置缓存头: @EnableWebMvc public class WebMvcConfiguration implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry .addResourceHandler("/resources/**") .addResourceLocations("/resources/") .setCachePeriod(31556926); } // ... } 历史上的浏览器,包括 Internet Explorer,将尝试使用内容嗅探来猜测请求的内容类型。这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改进用户体验。例如,如果浏览器遇到没有指定内容类型的 JavaScript 文件,那么它将能够猜测内容类型,然后执行它。
内容嗅探的问题在于,这允许恶意用户使用多语种(即作为多种内容类型有效的文件)来执行 XSS 攻击。例如,一些网站可能允许用户向网站提交有效的 postscript 文档并查看它。恶意用户可能会创建一个 postscript 文档,该文档也是一个有效的JavaScript文件,并使用它执行 XSS 攻击。 可以通过在我们的响应中添加以下报头来禁用内容嗅探: X-Content-Type-Options: nosniff 与缓存控制元素一样,当使用 <headers> 元素时默认添加 nosniff 指令,而且没有子元素。但是,如果希望对添加哪些报头进行更多控制,可以使用 <content-type-options> 元素和 headers@defaults-disabled 属性,如下所示: <http> <!-- ... --> <headers defaults-disabled="true"> <content-type-options /> </headers> </http> 在 Spring Security Java 配置中默认添加 X-Content-Type-Options 报头。如果你希望对报头进行更多的控制,可以显式指定以下内容类型选项: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .defaultsDisabled() .contentTypeOptions(); } } 当你输入银行的网站时,你是否输入了 mybank.example.com,还是输入了 https://mybank.example.com?如果省略了 HTTPS 协议,则可能受到中间人攻击的影响。即使网站执行到 https://mybank.example.com 的重定向,恶意用户可以拦截初始 HTTP 请求并操作响应(即,重定向到 https://mibank.example.com 并窃取他们的凭据)。 许多用户省略了 HTTPS 协议,这就是为什么创建 HTTP 严格传输安全(HSTS)的原因。一旦 mybank.example.com 被添加为 HSTS 主机,浏览器就可以提前知道对 mybank.example.com 的任何请求都应该被解释为 https://mybank.example.com。这大大减少了中间人攻击发生的可能性。
将站点标记为 HSTS 主机的一种方法是将主机预加载到浏览器中。另一种方法是向响应添加 "Strict-Transport-Security" 报头。例如,以下将指示浏览器将该域作为 HSTS 主机处理一年(一年大约有 31536000 秒): Strict-Transport-Security: max-age=31536000 ; includeSubDomains 可选的 includeSubDomains 指令指示 Spring Security 子域(即,secure.mybank.example.com)也应该被当作 HSTS 域。 与其他报头一样,Spring Security 在默认情况下添加了 HSTS。你可以用下面的 <hsts> 元素自定义 HSTS 报头: <http> <!-- ... --> <headers> <hsts include-subdomains="true" max-age-seconds="31536000" /> </headers> </http> 类似地,你使用 Java 配置只启用 HSTS 报头: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .httpStrictTransportSecurity() .includeSubdomains(true) .maxAgeSeconds(31536000); } } HTTP 公钥填塞(HPKP)是一种安全特性,它告诉 web 客户端将特定的加密公钥与某个 web 服务器相关联,以防止中间人(MITM)使用伪造证书进行攻击。 为了确保在 TLS 会话中使用的服务器的公钥的真实性,该公钥被包装到一个 X.509 证书中,该证书通常由证书颁发机构(CA)签名。像浏览器这样的 web 客户端信任很多这样的 CA,它们都可以为任意域名创建证书。如果攻击者能够妥协单个 CA,他们可以对各种 TLS 连接执行 MITM 攻击。HPKP 可以通过告诉客户端哪个公钥属于某个 web 服务器来规避 HTTPS 协议的这种威胁。HPKP 是第一次使用(TOFU)技术的信任。当 web 服务器第一次通过一个特殊的 HTTP 报头告诉客户端哪些公钥属于它时,客户端会在给定的时间段内存储这些信息。当客户端再次访问服务器时,它期望一个包含公钥的证书,其指纹已经通过 HPKP 知道。如果服务器提供未知的公钥,客户端应该向用户发出警告。
为站点启用该特性非常简单,只要在通过 HTTPS 访问站点时返回 Public-Key-Pins HTTP 报头即可。例如,以下将指示用户代理仅向给定 URI(通过 report-uri 指令)报告两个 pin 的 pin 验证失败: Public-Key-Pins-Report-Only: max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=" ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="http://example.net/pkp-report" ; includeSubDomains pin 验证失败报告 是一个标准的 JSON 结构,可以由 web 应用程序自己的 API 或由公共托管的 HPKP 报告服务(例如,REPORT-URI)捕获。 可选的 includeSubDomains 指令指示浏览器也用给定的 <hpkp> 验证子域。 与其他报头相反,Spring Security 在默认情况下不添加 HPKP。你可以用下面的 <hpkp> 元素自定义 HPKP 报头: <http> <!-- ... --> <headers> <hpkp include-subdomains="true" report-uri="http://example.net/pkp-report"> <pins> <pin algorithm="sha256">d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=</pin> <pin algorithm="sha256">E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=</pin> </pins> </hpkp> </headers> </http> 类似地,可以使用 Java 配置启用 HPKP 报头: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .httpPublicKeyPinning() .includeSubdomains(true) .reportUri("http://example.net/pkp-report") .addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="; } } 允许你的网站被添加到一个 frame 可能是一个安全问题。例如,使用聪明的 CSS 样式可以欺骗用户点击他们不想要的东西(视频演示)。例如,登录到他们的银行的用户可能会点击一个允许其他用户访问的按钮。这种攻击被称为点击劫持。
有多种方法可以减轻点击劫持攻击。例如,为了保护传统浏览器免受点击劫持攻击,可以使用frame 破坏代码。虽然不是完美的,但 frame 破坏代码是你可以为传统浏览器做的最好的。 一种更现代的解决点击劫持的方法是使用 X-Frame-Options 报头: X-Frame-Options: DENY X-Frame-Options 响应报头指示浏览器防止响应中具有该头的任何站点在 frame 内呈现。默认情况下,Spring Security 禁用 iframe 中的呈现。 你可以使用 frame-options 元素自定义 X-Frame-Options。 <http> <!-- ... --> <headers> <frame-options policy="SAMEORIGIN" /> </headers> </http> 类似地,你可以使用以下配置自定义 frame 选项,以便在 Java 配置中使用相同的源: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .frameOptions() .sameOrigin(); } } 一些浏览器支持过滤反射的 XSS 攻击。这绝对不是万无一失的,但确实有助于 XSS 保护。 过滤通常是默认启用的,所以添加报头通常只是确保启用了它,并指示浏览器在检测到 XSS 攻击时做什么。例如,过滤器可能尝试以最少侵入的方式改变内容,以便仍然呈现一切。有时,这种类型的替换 本身可以成为 XSS 漏洞。相反,最好是阻止内容,而不是试图修复它。为此,我们可以添加以下报头: X-XSS-Protection: 1; mode=block 默认情况下包含此报头。然而,我们可以定制我们想要的。例如: <http> <!-- ... --> <headers> <xss-protection block="false"/> </headers> </http> 同样,你可以在 Java 配置中自定义 XSS 保护: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .xssProtection() .block(false); } } 内容安全策略 (CSP) 是一种机制,web 应用程序可以利用该机制来减轻内容注入漏洞,例如跨站点脚本(XSS)。CSP 是一种声明性策略,它为 web 应用程序作者提供了一种工具,用于声明并最终通知客户端(用户代理)web 应用程序期望从其加载资源的源。
web 应用程序可以通过在响应中包括以下 HTTP 报头中的一个来使用 CSP:
这些报头中的每一个都被用作向客户端传递安全策略的机制。安全策略包含一组安全策略指令(例如,script-src 和 object-src),每个指令负责声明特定资源表示的限制。 例如,web 应用程序可以通过在响应中包括以下报头来声明它期望从特定的、可信的源加载脚本: Content-Security-Policy: script-src https://trustedscripts.example.com 从 script-src 指令中声明的内容之外的其他源加载脚本的尝试将被用户代理阻止。另外,如果在安全策略中声明了 report-uri 指令,那么用户代理将向声明的 URL 报告违反。 例如,如果 web 应用程序违反了声明的安全策略,则下面的响应报头将指示用户代理将违反报告发送到策略 report-uri 指令中指定的 URL。 Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/ 违规报告是标准的 JSON 结构,可以由 web 应用程序自己的 API 或由公共托管的 CSP 违规报告服务(例如,REPORT-URI)捕获。 Content-Security-Policy-Report-Only 报头为 web 应用程序作者和管理员提供了监视安全策略而不是强制执行它们的能力。该报头通常用于实验和/或开发站点的安全策略。当一个策略被认为有效时,它可以通过使用 Content-Security-Policy 报头字段来强制执行。 给定下面的响应报头,策略声明脚本可以从两个可能的源之一加载。 Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/ 如果站点违反此策略,则通过尝试从 evil.com 加载脚本,用户代理将向 report-uri 指令指定的声明 URL 发送违规报告,但仍然允许加载违规资源。 重要的是要注意,Spring Security 在默认情况下不添加内容安全策略。web 应用程序作者必须声明用于保护和/或监视受保护资源的安全策略。 例如,给出以下安全策略: script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/ 你可以使用 XML 配置的 <content-security-policy> 启用 CSP 报头,如下所示: <http> <!-- ... --> <headers> <content-security-policy policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" /> </headers> </http> 若要启用 CSP 'report-only' 报头,请将该元素配置如下: <http> <!-- ... --> <headers> <content-security-policy policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" report-only="true" /> </headers> </http> 类似地,你可以使用 Java 配置启用 CSP 报头,如下所示: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"); } } 若要启用 CSP 'report-only' 报头,请提供以下 Java 配置: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") .reportOnly(); } } 将内容安全策略应用到 web 应用程序通常是一项不平凡的任务。下列资源可以为开发网站的有效安全策略提供进一步的帮助。 引用策略是一种机制,web 应用程序可以利用该机制来管理 referrer 字段,该字段包含用户所打开的最后一个页面。 Spring Security 的方法是使用引用策略 报头,它提供不同的策略: Referrer-Policy: same-origin Referrer-Policy 策略响应报头指示浏览器允许目的地知道用户先前所在的源。 Spring Security 在默认情况下不添加引用策略报头。 可以使用 XML 配置中的 <referrer-policy> 元素启用引用策略报头,如下所示: <http> <!-- ... --> <headers> <referrer-policy policy="same-origin" /> </headers> </http> 类似地,你可以使用 Java 配置启用引用策略头,如下所示: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .referrerPolicy(ReferrerPolicy.SAME_ORIGIN); } } 特性策略是一种机制,允许 Web 开发人员选择性地启用、禁用和修改浏览器中某些 API 和 Web 特性的行为。 Feature-Policy: geolocation 'self' 使用特性策略,开发人员可以选择进入一组策略,让浏览器对整个站点使用的特定特性进行强制执行。这些策略限制了站点可以访问或修改浏览器对某些特征的默认行为。 Spring Security 在默认情况下不添加特性策略报头。 你可以使用 XML 配置中的 <feature-policy> 元素启用特性策略报头,如下所示: <http> <!-- ... --> <headers> <feature-policy policy-directives="geolocation 'self'" /> </headers> </http> 类似地,你可以使用 Java 配置启用特性策略报头,如下所示: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .featurePolicy("geolocation 'self'"); } } Spring Security 有一些机制可以方便地将更常见的安全报头添加到应用程序中。但是,它还提供钩子来启用自定义报头。 有时你可能希望将自定义安全报头注入到应用程序中,这些应用程序支持不是非常。例如,给定以下自定义安全报头: X-Custom-Security-Header: header-value 当使用 XML 命名空间时,可以使用下面的 <header> 元素将这些报头添加到响应中: <http> <!-- ... --> <headers> <header name="X-Custom-Security-Header" value="header-value"/> </headers> </http> 类似地,可以使用 Java 配置将报头添加到响应中,如下所示: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value")); } }
当命名空间或 Java 配置不支持所需的报头时,你可以创建自定义
让我们来看一个使用 <http> <!-- ... --> <headers> <header ref="frameOptionsWriter"/> </headers> </http> <!-- Requires the c-namespace. See http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-c-namespace --> <beans:bean id="frameOptionsWriter" class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter" c:frameOptionsMode="SAMEORIGIN"/> 我们还可以用 Java 配置限制内容的编排到同一源: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // ... .headers() .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN)); } }
有时你可能只想为某些请求写一个报头。例如,你可能只希望保护登录页面不被框架化。你可以使用 <http> <!-- ... --> <headers> <frame-options disabled="true"/> <header ref="headerWriter"/> </headers> </http> <beans:bean id="headerWriter" class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter"> <beans:constructor-arg> <bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher" c:pattern="/login"/> </beans:constructor-arg> <beans:constructor-arg> <beans:bean class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"/> </beans:constructor-arg> </beans:bean> 我们还可以使用 Java 配置防止将登录页面的内容框架化: @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { RequestMatcher matcher = new AntPathRequestMatcher("/login"); DelegatingRequestMatcherHeaderWriter headerWriter = new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter()); http // ... .headers() .frameOptions().disabled() .addHeaderWriter(headerWriter); } } |