跨站请求伪造 (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中,而不是会话中
-
我想选择退出延迟令牌
-
我需要关于将Thymeleaf、JSP或其他视图技术与后端集成的指导
-
我需要关于将Angular或其他JavaScript框架与后端集成的指导
-
我需要关于将移动应用程序或其他客户端与后端集成的指导
-
我需要关于处理错误的指导
-
我想测试CSRF防护
-
我需要关于禁用CSRF防护的指导
理解 CSRF 保护的组成部分
CSRF保护由多个组件共同实现,这些组件集成在CsrfFilter中:

图 1. CsrfFilter 组件
CSRF 防护分为两部分:
-
通过委托给 CsrfTokenRequestHandler,使应用程序能够获取 CsrfToken。
-
判断请求是否需要 CSRF 保护,加载并验证令牌,并处理 AccessDeniedException。

图 2. CsrfFilter 处理流程
-
1 首先,加载 DeferredCsrfToken,它持有对 CsrfTokenRepository 的引用,以便稍后(在 4 中)加载持久化的
CsrfToken。 -
2 其次,将一个
Supplier<CsrfToken>(由DeferredCsrfToken创建)提供给 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 保护方面发生变化的概述:
-
为提高性能,
CsrfToken的加载现在默认延迟,不再需要在每个请求上都加载会话。 -
CsrfToken现在默认在每个请求中包含随机性,以保护 CSRF 令牌免受 BREACH 攻击。
Spring Security 6 的变更要求对单页应用进行额外配置,因此你可能会发现 单页应用 这一节特别有用。
持久化 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
你可以使用 CookieCsrfTokenRepository 将 CsrfToken 持久化到 cookie 中,以支持基于 JavaScript 的应用程序。
CookieCsrfTokenRepository 默认会向名为 XSRF-TOKEN 的 Cookie 写入数据,并从名为 X-XSRF-TOKEN 的 HTTP 请求头或请求参数 _csrf 中读取数据。这些默认值源自 Angular 及其前身 AngularJS。
有关此主题的最新信息,请参阅 HttpClient XSRF/CSRF 安全 和 withXsrfConfiguration。
你可以通过以下配置来配置 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()。此名称不可配置,但可以通过 XorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 来更改 _csrf 这个名称。
此实现还会从请求中解析令牌值,来源可以是请求头(默认为 X-CSRF-TOKEN 或 X-XSRF-TOKEN 之一)或请求参数(默认为 _csrf)。
通过在 CSRF 令牌值中编码随机性来提供 BREACH 保护,确保每次请求返回的 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()。此名称不可配置,但可以通过 CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 来更改 _csrf 这一名称。
此实现还会从请求中解析令牌值,来源可以是请求头(默认是 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,直到需要时才进行加载。
每当使用不安全的 HTTP 方法(例如 POST)发起请求时,都需要 CsrfToken。此外,任何需要在响应中渲染令牌的请求也需要它,例如包含隐藏 CSRF 令牌 <input> 标签的 <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,您可以将CSRF令牌放在HTTP请求头中提交,而不是作为请求参数。
为了获取CSRF令牌,你可以配置Spring Security将预期的CSRF令牌存储在cookie中。通过将预期令牌存储在cookie中,像Angular这样的JavaScript框架可以自动将实际的CSRF令牌作为HTTP请求头包含进去。
将单页应用(SPA)与 Spring Security 的 CSRF 保护集成时,对于 BREACH 保护和延迟令牌有特殊注意事项。完整的配置示例请参见下一节。
你可以在以下章节中阅读有关不同类型 JavaScript 应用程序的内容:
单页应用程序
将单页应用程序(SPA)与 Spring Security 的 CSRF 防护集成时,存在一些特殊的注意事项。
请注意,Spring Security 默认提供 BREACH 对 CsrfToken 的保护。当将预期的 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.spa());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
spa()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf>
<spa />
</csrf>
</http>
多页面应用
对于多页面应用,如果每个页面都加载了JavaScript,除了将CSRF令牌存储在cookie中之外,另一种方法是将CSRF令牌包含在meta标签内。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>
一旦元标签包含CSRF令牌,JavaScript代码就可以读取元标签并将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响应头中。
实现这一点的一种方法是使用 @ControllerAdvice 配合 CsrfTokenArgumentResolver。以下是一个适用于应用程序中所有控制器端点的 @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 时,这通常不是问题。
请务必记住,控制器端点(controller endpoints)和控制器通知(controller advice)是在 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保护,并配置忽略规则,如下例所示:
- 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.config.http.PathPatternRequestMatcherFactoryBean">
<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(PathPatternRequestMatcher.withDefaults().matcher("/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 = PathPatternRequestMatcher.withDefaults().matcher("/logout")
}
}
return http.build()
}
}
更多信息请参见登出章节。
CSRF 与会话超时
默认情况下,Spring Security 使用 HttpSessionCsrfTokenRepository 将 CSRF 令牌存储在 HttpSession 中。这可能导致会话过期后,没有 CSRF 令牌可供验证的情况。
我们已经讨论了会话超时的一般解决方案。本节将讨论与 Servlet 支持相关的 CSRF 超时的具体细节。
您可以将 CSRF 令牌的存储方式更改为存储在 Cookie 中。具体细节请参阅 使用 CookieCsrfTokenRepository 一节。
如果令牌过期,您可能希望通过指定一个自定义的AccessDeniedHandler来自定义其处理方式。自定义的AccessDeniedHandler可以按照您希望的任何方式处理InvalidCsrfTokenException。
多部分(文件上传)
我们已经讨论过保护多部分请求(文件上传)免受 CSRF 攻击会导致一个先有鸡还是先有蛋的问题。当 JavaScript 可用时,我们建议将 CSRF 令牌包含在 HTTP 请求头中来规避此问题。
关于在 Spring 中使用多部分表单的更多信息,请参阅 Spring 参考文档中的 Multipart Resolver 部分以及 MultipartFilter javadoc。
将 CSRF 令牌置于请求体中
我们已经讨论过将 CSRF 令牌放置在请求体中的权衡。在本节中,我们将讨论如何配置 Spring Security 以从请求体中读取 CSRF 令牌。
要从请求体中读取CSRF令牌,需将MultipartFilter配置在Spring Security过滤器之前。将MultipartFilter置于Spring Security过滤器之前意味着调用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方法转换部分找到更多信息。