跨站请求伪造 (CSRF)
在用户可以登录的应用程序中,考虑如何防范跨站请求伪造 (CSRF)是非常重要的。
Spring Security 默认为不安全的 HTTP 方法(例如 POST 请求)提供 CSRF 攻击防护,因此不需要额外的代码。您可以使用以下方法显式指定默认配置:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf { }
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf/>
</http>
要了解更多关于应用程序的CSRF保护,请考虑以下用例:
-
我想将 CsrfToken 存储在 cookie 中,而不是会话中
-
我想将 CsrfToken 存储在自定义位置
-
我需要关于将 Thymeleaf、JSP 或其他视图技术 与后端集成的指导
-
我需要关于将 Angular 或其他 JavaScript 框架 与后端集成的指导
-
我需要关于将 移动应用程序或其他客户端 与后端集成的指导
-
我需要关于处理错误的指导
-
我需要关于禁用 CSRF 保护的指导
了解CSRF保护的组件
CSRF 保护由几个组件提供,这些组件在 CsrfFilter 中组合而成:
图 1. CsrfFilter
组件
CSRF 保护分为两个部分:
-
通过委托给 CsrfTokenRequestHandler,使 CsrfToken 对应用程序可用。
-
确定请求是否需要 CSRF 保护,加载并验证令牌,并处理 AccessDeniedException。
图 2. CsrfFilter
处理流程
-
1 首先,加载 DeferredCsrfToken,它持有对 CsrfTokenRepository 的引用,以便稍后(在 4 中)可以加载持久化的
CsrfToken
。 -
2 其次,将从
DeferredCsrfToken
创建的Supplier<CsrfToken>
传递给 CsrfTokenRequestHandler,后者负责填充请求属性,使CsrfToken
对应用程序的其余部分可用。 -
3 接下来,开始主要的 CSRF 保护处理,并检查当前请求是否需要 CSRF 保护。如果不需要,则继续过滤器链并结束处理。
-
4 如果需要 CSRF 保护,最终会从
DeferredCsrfToken
中加载持久化的CsrfToken
。 -
5 继续,使用 CsrfTokenRequestHandler 解析客户端提供的实际 CSRF 令牌(如果有)。
-
6 将实际的 CSRF 令牌与持久化的
CsrfToken
进行比较。如果有效,则继续过滤器链并结束处理。 -
7 如果实际的 CSRF 令牌无效(或缺失),则将
AccessDeniedException
传递给 AccessDeniedHandler 并结束处理。
迁移到 Spring Security 6
在从Spring Security 5迁移到6时,有一些变化可能会影响你的应用程序。以下是Spring Security 6中CSRF保护方面发生变化的概述:
Spring Security 6 中的变化要求为单页面应用程序进行额外的配置,因此你可能会发现 单页面应用程序 部分特别有用。
请参阅迁移章节中的Exploit Protection部分,以获取有关迁移 Spring Security 5 应用程序的更多信息。
持久化 CsrfToken
CsrfToken
使用 CsrfTokenRepository
进行持久化。
默认情况下,使用 HttpSessionCsrfTokenRepository 将令牌存储在会话中。Spring Security 还提供了 CookieCsrfTokenRepository 用于将令牌存储在 cookie 中。您还可以指定 您自己的实现 来将令牌存储在任何您喜欢的位置。
使用 HttpSessionCsrfTokenRepository
默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository 将预期的 CSRF 令牌存储在 HttpSession
中,因此不需要额外的代码。
HttpSessionCsrfTokenRepository
从会话(无论是内存、缓存还是数据库)中读取令牌。如果需要直接访问会话属性,请先使用 HttpSessionCsrfTokenRepository#setSessionAttributeName
配置会话属性名称。
您可以使用以下配置显式指定默认配置:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(new HttpSessionCsrfTokenRepository())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = HttpSessionCsrfTokenRepository()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository"/>
使用 CookieCsrfTokenRepository
你可以将 CsrfToken
持久化到 cookie 中,以使用 CookieCsrfTokenRepository 来支持基于 JavaScript 的应用程序。
CookieCsrfTokenRepository
默认将数据写入名为 XSRF-TOKEN
的 cookie 中,并从名为 X-XSRF-TOKEN
的 HTTP 请求头或请求参数 _csrf
中读取数据。这些默认设置来自 Angular 及其前身 AngularJS。
参阅跨站请求伪造 (XSRF) 防护指南和 HttpClientXsrfModule,以获取有关此主题的最新信息。
您可以使用以下配置来配置 CookieCsrfTokenRepository
:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
示例中显式将 HttpOnly
设置为 false
。这是为了让 JavaScript 框架(如 Angular)能够读取它。如果你不需要直接使用 JavaScript 读取 cookie 的功能,我们建议省略 HttpOnly
(通过使用 new CookieCsrfTokenRepository()
代替)以提高安全性。
自定义 CsrfTokenRepository
有时你可能需要实现一个自定义的 CsrfTokenRepository。
一旦你实现了 CsrfTokenRepository
接口,你可以通过以下配置让 Spring Security 使用它:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(new CustomCsrfTokenRepository())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = CustomCsrfTokenRepository()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="example.CustomCsrfTokenRepository"/>
处理 CsrfToken
CsrfToken
通过 CsrfTokenRequestHandler
提供给应用程序。该组件还负责从 HTTP 头或请求参数中解析 CsrfToken
。
默认情况下,使用 XorCsrfTokenRequestAttributeHandler 为 CsrfToken
提供 BREACH 保护。Spring Security 还提供了 CsrfTokenRequestAttributeHandler,用于选择不使用 BREACH 保护。您还可以指定 您自己的实现 来自定义处理和解析令牌的策略。
使用 XorCsrfTokenRequestAttributeHandler
(BREACH)
XorCsrfTokenRequestAttributeHandler
会将 CsrfToken
作为名为 _csrf
的 HttpServletRequest
属性提供,并且还为 BREACH 提供了防护。
CsrfToken
也作为请求属性使用 CsrfToken.class.getName()
的名称提供。这个名称是不可配置的,但 _csrf
这个名称可以使用 XorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName
进行更改。
此实现还从请求中解析令牌值,作为请求头(默认为 X-CSRF-TOKEN 或 X-XSRF-TOKEN 之一)或请求参数(默认为 _csrf
)。
BREACH 防护通过将随机性编码到 CSRF 令牌值中来实现,以确保返回的 CsrfToken
在每次请求时都会发生变化。当令牌稍后被解析为头部值或请求参数时,它会被解码以获取原始令牌,然后与持久化的 CsrfToken 进行比较。
Spring Security 默认保护 CSRF 令牌免受 BREACH 攻击,因此不需要额外的代码。你可以使用以下配置显式指定默认配置:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler"/>
使用 CsrfTokenRequestAttributeHandler
CsrfTokenRequestAttributeHandler
会将 CsrfToken
作为名为 _csrf
的 HttpServletRequest
属性提供。
CsrfToken
也作为请求属性使用 CsrfToken.class.getName()
提供。这个名称是不可配置的,但是 _csrf
这个名称可以使用 CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName
进行更改。
此实现还从请求中解析令牌值,可以是请求头(默认为 X-CSRF-TOKEN 或 X-XSRF-TOKEN 之一)或请求参数(默认为 _csrf
)。
CsrfTokenRequestAttributeHandler
的主要用途是选择不使用 CsrfToken
的 BREACH 保护,可以通过以下配置来设置:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler"/>
自定义 CsrfTokenRequestHandler
你可以实现 CsrfTokenRequestHandler
接口来自定义处理和解析令牌的策略。
CsrfTokenRequestHandler
接口是一个 @FunctionalInterface
,可以使用 lambda 表达式来实现以自定义请求处理。你需要实现整个接口来自定义如何从请求中解析令牌。有关使用委托来实现自定义处理和解析令牌策略的示例,请参阅 为单页应用程序配置 CSRF。
一旦你实现了 CsrfTokenRequestHandler
接口,你可以通过以下配置让 Spring Security 使用它:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new CustomCsrfTokenRequestHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = CustomCsrfTokenRequestHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="example.CustomCsrfTokenRequestHandler"/>
CsrfToken
的延迟加载
默认情况下,Spring Security 会在需要时才加载 CsrfToken
。
CsrfToken
在使用不安全的 HTTP 方法(如 POST)发起请求时是必需的。此外,任何将该令牌渲染到响应中的请求也需要它,例如包含一个隐藏的 <input>
用于 CSRF 令牌的 <form>
标签的网页。
由于 Spring Security 默认也将 CsrfToken
存储在 HttpSession
中,延迟 CSRF 令牌可以通过不必在每个请求上都加载会话来提高性能。
如果您希望选择不使用延迟令牌,并在每次请求时都加载 CsrfToken
,则可以通过以下配置来实现:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null);
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(requestHandler)
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val requestHandler = XorCsrfTokenRequestAttributeHandler()
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null)
http {
// ...
csrf {
csrfTokenRequestHandler = requestHandler
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler">
<b:property name="csrfRequestAttributeName">
<b:null/>
</b:property>
</b:bean>
通过将 csrfRequestAttributeName
设置为 null
,必须首先加载 CsrfToken
以确定使用哪个属性名。这会导致每次请求时都加载 CsrfToken
。
集成 CSRF 保护
为了使用同步令牌模式来防范CSRF攻击,我们必须在HTTP请求中包含实际的CSRF令牌。这必须包含在请求的一部分(如表单参数、HTTP头部或其他部分)中,而这些部分不会由浏览器自动包含在HTTP请求中。
以下各节描述了前端或客户端应用程序可以与受CSRF保护的后端应用程序集成的各种方式:
HTML 表单
要提交一个 HTML 表单,CSRF 令牌必须作为隐藏输入包含在表单中。例如,渲染后的 HTML 可能如下所示:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
以下视图技术会自动在具有不安全 HTTP 方法(如 POST)的表单中包含实际的 CSRF 令牌:
-
任何其他与 RequestDataValueProcessor 集成的视图技术(通过 CsrfRequestDataValueProcessor)
-
您也可以通过 csrfInput 标签自行包含令牌
如果这些选项不可用,你可以利用 CsrfToken
作为 名为 _csrf 的 HttpServletRequest 属性 这一事实。以下示例使用 JSP 来实现这一点:
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form>
JavaScript 应用
JavaScript 应用程序通常使用 JSON 而不是 HTML。如果你使用 JSON,可以在 HTTP 请求头中提交 CSRF 令牌,而不是作为请求参数。
为了获取CSRF令牌,你可以配置Spring Security将预期的CSRF令牌存储在cookie中。通过将预期的令牌存储在cookie中,像Angular这样的JavaScript框架可以自动将实际的CSRF令牌作为HTTP请求头包含进去。
在将单页应用程序 (SPA) 与 Spring Security 的 CSRF 保护集成时,需要特别考虑 BREACH 保护和延迟令牌。完整的配置示例请参见下一节。
你可以在以下章节中了解不同类型的 JavaScript 应用程序:
单页面应用程序
将单页应用程序(SPA)与 Spring Security 的 CSRF 保护集成时,有一些特别的注意事项。
回想一下,Spring Security 默认提供了 CSRF 令牌的 BREACH 防护。当将预期的 CSRF 令牌 存储在 cookie 中 时,JavaScript 应用程序只能访问纯令牌值,而无法访问编码后的值。需要提供一个 自定义请求处理器 来解析实际的令牌值。
此外,存储CSRF令牌的cookie将在认证成功和注销成功时被清除。Spring Security默认延迟加载新的CSRF令牌,因此需要额外的工作来返回一个新的cookie。
在认证成功和注销成功后刷新令牌是必需的,因为 CsrfAuthenticationStrategy 和 CsrfLogoutHandler 会清除之前的令牌。客户端应用程序在没有获取新的令牌的情况下将无法执行不安全的 HTTP 请求,例如 POST 请求。
为了轻松地将单页应用程序与 Spring Security 集成,可以使用以下配置:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 1
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) 2
);
return http.build();
}
}
final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
this.xor.handle(request, response, csrfToken);
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get();
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() 1
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() 2
}
}
return http.build()
}
}
class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
private val xor: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
xor.handle(request, response, csrfToken)
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get()
}
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
val headerValue = request.getHeader(csrfToken.headerName)
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*/
return if (StringUtils.hasText(headerValue)) {
plain
} else {
/*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
xor
}.resolveCsrfTokenValue(request, csrfToken)
}
}
<http>
<!-- ... -->
<csrf
token-repository-ref="tokenRepository" // <1>
request-handler-ref="requestHandler"/> // <2>
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
class="example.SpaCsrfTokenRequestHandler"/>
配置
CookieCsrfTokenRepository
,将HttpOnly
设置为false
,以便 JavaScript 应用程序可以读取该 cookie。配置一个自定义的
CsrfTokenRequestHandler
,根据是否是 HTTP 请求头 (X-XSRF-TOKEN
) 或请求参数 (_csrf
) 来解析 CSRF 令牌。此实现还会导致在每个请求中加载延迟的CsrfToken
,如果需要,它将返回一个新的 cookie。
多页面应用程序
对于多页面应用程序,每次加载页面时都会加载 JavaScript,在这种情况下,可以将 CSRF 令牌包含在 meta
标签中,而不是通过cookie来暴露。HTML 可能看起来像这样:
<html>
<head>
<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
<!-- ... -->
</head>
<!-- ... -->
</html>
为了在请求中包含CSRF令牌,你可以利用 CsrfToken
作为 名为 _csrf 的 HttpServletRequest 属性 这一事实。以下示例通过 JSP 实现了这一点:
<html>
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<!-- ... -->
</head>
<!-- ... -->
</html>
一旦 meta 标签中包含 CSRF 令牌,JavaScript 代码就可以读取这些 meta 标签,并将 CSRF 令牌作为头部包含进去。如果你使用 jQuery,可以使用以下代码来实现这一点:
$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
});
其他 JavaScript 应用
JavaScript 应用程序的另一个选项是将 CSRF 令牌包含在 HTTP 响应头中。
实现这一目标的一种方法是通过使用带有 CsrfTokenArgumentResolver 的 @ControllerAdvice
。以下是一个适用于应用程序中所有控制器端点的 @ControllerAdvice
示例:
- Java
- Kotlin
@ControllerAdvice
public class CsrfControllerAdvice {
@ModelAttribute
public void getCsrfToken(HttpServletResponse response, CsrfToken csrfToken) {
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
}
}
@ControllerAdvice
class CsrfControllerAdvice {
@ModelAttribute
fun getCsrfToken(response: HttpServletResponse, csrfToken: CsrfToken) {
response.setHeader(csrfToken.headerName, csrfToken.token)
}
}
因为此 @ControllerAdvice
适用于应用程序中的所有端点,所以它会导致每次请求时都加载 CSRF 令牌,这在使用 HttpSessionCsrfTokenRepository 时可能会抵消延迟令牌的好处。但是,当使用 CookieCsrfTokenRepository 时,这通常不是问题。
需要注意的是,控制器端点和控制器增强是在 Spring Security 过滤器链之后被调用的。这意味着,只有当请求通过过滤器链到达你的应用程序时,这个 @ControllerAdvice
才会被应用。有关向过滤器链中添加过滤器以更早访问 HttpServletResponse
的示例,请参见单页应用程序的配置。
CSRF 令牌现在将在响应头中可用(默认为 X-CSRF-TOKEN 或 X-XSRF-TOKEN),适用于控制器通知应用的任何自定义端点。可以使用对后端的任何请求从响应中获取令牌,并且可以在后续请求中在具有相同名称的请求头中包含该令牌。
移动应用程序
像JavaScript 应用程序一样,移动应用程序通常使用 JSON 而不是 HTML。不处理浏览器流量的后端应用程序可以选择禁用 CSRF。在这种情况下,不需要额外的工作。
然而,一个同时服务于浏览器流量因此仍然需要 CSRF 保护的后端应用程序可以继续将 CsrfToken
存储在会话中,而不是存储在 cookie 中。
在这种情况下,与后端集成的典型模式是暴露一个 /csrf
端点,以便前端(移动或浏览器客户端)按需请求CSRF令牌。使用这种模式的好处是CSRF令牌可以继续被延迟加载,并且只有在请求需要CSRF保护时才需要从会话中加载。使用自定义端点还意味着客户端应用程序可以通过发出显式请求来按需生成新的令牌(如果必要)。
这种模式可以用于任何需要CSRF保护的应用程序,而不仅仅是移动应用程序。虽然在这些情况下通常不需要这种方法,但它提供了另一种与具有CSRF保护的后端集成的选项。
以下是一个使用 CsrfTokenArgumentResolver 的 /csrf
端点的示例:
- Java
- Kotlin
@RestController
public class CsrfController {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken csrfToken) {
return csrfToken;
}
}
@RestController
class CsrfController {
@GetMapping("/csrf")
fun csrf(csrfToken: CsrfToken): CsrfToken {
return csrfToken
}
}
如果需要在与服务器进行身份验证之前使用上述端点,您可以考虑添加 .requestMatchers("/csrf").permitAll()
。
此端点应在应用程序启动或初始化时(例如,在加载时)调用以获取CSRF令牌,并且在认证成功和注销成功后也应调用。
在认证成功和登出成功后刷新令牌是必需的,因为 CsrfAuthenticationStrategy 和 CsrfLogoutHandler 会清除之前的令牌。客户端应用程序在没有获取新的令牌的情况下将无法执行不安全的 HTTP 请求,例如 POST 请求。
一旦你获得了CSRF令牌,你需要将其作为HTTP请求头(默认为X-CSRF-TOKEN或X-XSRF-TOKEN之一)自行包含。
处理 AccessDeniedException
要处理诸如 InvalidCsrfTokenException
这样的 AccessDeniedException
,你可以配置 Spring Security 以你喜欢的任何方式来处理这些异常。例如,你可以使用以下配置来设置一个自定义的访问拒绝页面:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling((exceptionHandling) -> exceptionHandling
.accessDeniedPage("/access-denied")
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
exceptionHandling {
accessDeniedPage = "/access-denied"
}
}
return http.build()
}
}
<http>
<!-- ... -->
<access-denied-handler error-page="/access-denied"/>
</http>
CSRF 测试
你可以使用 Spring Security 的 测试支持 和 CsrfRequestPostProcessor 来测试 CSRF 保护,如下所示:
- Java
- Kotlin
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfTests {
private MockMvc mockMvc;
@BeforeEach
public void setUp(WebApplicationContext applicationContext) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.build();
}
@Test
public void loginWhenValidCsrfTokenThenSuccess() throws Exception {
this.mockMvc.perform(post("/login").with(csrf())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, "/"));
}
@Test
public void loginWhenInvalidCsrfTokenThenForbidden() throws Exception {
this.mockMvc.perform(post("/login").with(csrf().useInvalidToken())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden());
}
@Test
public void loginWhenMissingCsrfTokenThenForbidden() throws Exception {
this.mockMvc.perform(post("/login")
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser
public void logoutWhenValidCsrfTokenThenSuccess() throws Exception {
this.mockMvc.perform(post("/logout").with(csrf())
.accept(MediaType.TEXT_HTML))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"));
}
}
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [SecurityConfig::class])
@WebAppConfiguration
class CsrfTests {
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp(applicationContext: WebApplicationContext) {
mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
}
@Test
fun loginWhenValidCsrfTokenThenSuccess() {
mockMvc.perform(post("/login").with(csrf())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().is3xxRedirection)
.andExpect(header().string(HttpHeaders.LOCATION, "/"))
}
@Test
fun loginWhenInvalidCsrfTokenThenForbidden() {
mockMvc.perform(post("/login").with(csrf().useInvalidToken())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden)
}
@Test
fun loginWhenMissingCsrfTokenThenForbidden() {
mockMvc.perform(post("/login")
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden)
}
@Test
@WithMockUser
@Throws(Exception::class)
fun logoutWhenValidCsrfTokenThenSuccess() {
mockMvc.perform(post("/logout").with(csrf())
.accept(MediaType.TEXT_HTML))
.andExpect(status().is3xxRedirection)
.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"))
}
}
禁用 CSRF 保护
默认情况下,CSRF 保护是启用的,这会影响与后端集成和测试你的应用程序。在禁用 CSRF 保护之前,请考虑它是否适合你的应用程序。
你还可以考虑是否只有某些端点不需要 CSRF 保护,并配置忽略规则,如下例所示:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.ignoringRequestMatchers("/api/*")
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
ignoringRequestMatchers("/api/*")
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-matcher-ref="csrfMatcher"/>
</http>
<b:bean id="csrfMatcher"
class="org.springframework.security.web.util.matcher.AndRequestMatcher">
<b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
<b:constructor-arg>
<b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
<b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<b:constructor-arg value="/api/*"/>
</b:bean>
</b:bean>
</b:constructor-arg>
</b:bean>
如果您需要禁用CSRF保护,可以使用以下配置:
- Java
- Kotlin
- XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf.disable());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
disable()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf disabled="true"/>
</http>
CSRF 考虑事项
在实现针对CSRF攻击的防护时,有一些特别需要注意的事项。本节将讨论这些注意事项,特别是与servlet环境相关的内容。有关更一般的讨论,请参见CSRF注意事项。
登录
重要的是要为登录请求启用 CSRF 保护,以防止伪造登录尝试。Spring Security 的 servlet 支持默认就提供了这一点。
登出
对于注销请求,启用 CSRF 保护 是很重要的,以防止伪造注销尝试。如果启用了 CSRF 保护(默认情况下是启用的),Spring Security 的 LogoutFilter
将仅处理 HTTP POST 请求。这确保了注销需要一个 CSRF 令牌,并且恶意用户不能强行使您的用户注销。
最简单的方法是使用表单来注销用户。如果您确实希望使用链接,可以使用 JavaScript 使链接执行 POST(可能是在隐藏的表单上)。对于禁用了 JavaScript 的浏览器,您可以选择让链接将用户带到一个执行 POST 的注销确认页面。
如果你确实想使用 HTTP GET 进行登出,你可以这样做。但是,请记住,这通常不被推荐。例如,以下代码会在使用任何 HTTP 方法请求 /logout
URL 时登出:
- Java
- Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.logout((logout) -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
logout {
logoutRequestMatcher = AntPathRequestMatcher("/logout")
}
}
return http.build()
}
}
请参阅注销章节以获取更多信息。
CSRF 和会话超时
默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository 将 CSRF 令牌存储在 HttpSession
中。这可能导致会话过期,从而没有 CSRF 令牌可供验证的情况。
我们已经讨论了通用解决方案来处理会话超时。本节将讨论与 servlet 支持相关的 CSRF 超时的具体情况。
您可以将CSRF令牌的存储改为在cookie中。有关详细信息,请参阅使用CookieCsrfTokenRepository部分。
如果令牌确实过期,您可能希望自定义如何处理它,方法是指定一个自定义的 AccessDeniedHandler。自定义的 AccessDeniedHandler
可以按照您喜欢的任何方式处理 InvalidCsrfTokenException
。
多部分(文件上传)
我们已经讨论过,保护多部分请求(文件上传)免受 CSRF 攻击会导致一个鸡生蛋还是蛋生鸡的问题。当 JavaScript 可用时,我们建议将 CSRF 令牌包含在 HTTP 请求头中,以避开这个问题。
您可以在 Spring 参考文档的 Multipart Resolver 部分和 MultipartFilter javadoc 中找到更多关于使用 multipart 表单与 Spring 的信息。
将CSRF令牌放在Body中
我们已经讨论过将CSRF令牌放置在主体中的权衡。在本节中,我们将讨论如何配置Spring Security以从主体中读取CSRF令牌。
为了从主体中读取CSRF令牌,MultipartFilter
在Spring Security过滤器之前被指定。在Spring Security过滤器之前指定MultipartFilter
意味着调用MultipartFilter
时没有授权,这意味着任何人都可以在你的服务器上放置临时文件。但是,只有授权用户才能提交由你的应用程序处理的文件。一般来说,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。
- Java
- Kotlin
- XML
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
class SecurityApplicationInitializer : AbstractSecurityWebApplicationInitializer() {
override fun beforeSpringSecurityFilterChain(servletContext: ServletContext?) {
insertFilters(servletContext, MultipartFilter())
}
}
<filter>
<filter-name>MultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>MultipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
要确保在 XML 配置中 MultipartFilter
在 Spring Security 过滤器之前指定,你可以确保 web.xml
文件中的 MultipartFilter
的 <filter-mapping>
元素位于 springSecurityFilterChain
之前。
在 URL 中包含 CSRF 令牌
如果让未授权用户上传临时文件是不可接受的,另一种方法是将 MultipartFilter
放在 Spring Security 过滤器之后,并在表单的 action 属性中包含 CSRF 作为查询参数。由于 CsrfToken
被暴露为 名为 _csrf 的 HttpServletRequest 属性,我们可以使用它来创建一个包含 CSRF 令牌的 action
。以下示例通过 JSP 实现了这一点:
<form method="post"
action="./upload?${_csrf.parameterName}=${_csrf.token}"
enctype="multipart/form-data">
HiddenHttpMethodFilter
我们已经讨论过将CSRF令牌放在主体中的权衡。
在 Spring 的 Servlet 支持中,通过使用 HiddenHttpMethodFilter 来覆盖 HTTP 方法。你可以在参考文档的 HTTP 方法转换 部分找到更多信息。
进一步阅读
现在你已经复习了CSRF保护,可以考虑学习更多关于exploit protection的知识,包括secure headers和HTTP firewall,或者继续学习如何测试你的应用程序。