跨站请求伪造 (CSRF) 针对 WebFlux 环境
本节讨论 Spring Security 的跨站请求伪造 (CSRF)在 WebFlux 环境中的支持。
使用 Spring Security CSRF 保护
使用 Spring Security 的 CSRF 保护的步骤如下:
使用正确的 HTTP 动词
防范CSRF攻击的第一步是确保你的网站使用正确的HTTP动词。详细内容请参阅安全方法必须为只读。
配置CSRF保护
下一步是在应用程序中配置 Spring Security 的 CSRF 保护。默认情况下,Spring Security 的 CSRF 保护是启用的,但你可能需要自定义配置。接下来的几个小节将介绍一些常见的自定义配置。
自定义 CsrfTokenRepository
默认情况下,Spring Security 使用 WebSessionServerCsrfTokenRepository
将预期的 CSRF 令牌存储在 WebSession
中。有时,您可能需要配置一个自定义的 ServerCsrfTokenRepository
。例如,您可能希望将 CsrfToken
持久化到 cookie 中,以支持基于 JavaScript 的应用程序。
默认情况下,CookieServerCsrfTokenRepository
会写入一个名为 XSRF-TOKEN
的 Cookie,并从名为 X-XSRF-TOKEN
的头部或 HTTP _csrf
参数中读取。这些默认设置来自 AngularJS
你可以在Java配置中配置 CookieServerCsrfTokenRepository
:
- Java
- Kotlin
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
csrf {
csrfTokenRepository = CookieServerCsrfTokenRepository.withHttpOnlyFalse()
}
}
}
前面的示例显式设置了 cookieHttpOnly=false
。这是为了让 JavaScript(在这种情况下是 AngularJS)能够读取它。如果您不需要直接使用 JavaScript 读取 cookie 的能力,我们建议省略 cookieHttpOnly=false
(而是使用 new CookieServerCsrfTokenRepository()
),以提高安全性。
禁用CSRF保护
默认情况下,CSRF 保护是启用的。但是,如果对您的应用程序来说合适,您可以禁用 CSRF 保护。
下面的 Java 配置将禁用 CSRF 保护。
- Java
- Kotlin
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.csrf(csrf -> csrf.disable()))
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
csrf {
disable()
}
}
}
配置 ServerCsrfTokenRequestHandler
Spring Security 的 CsrfWebFilter 借助 ServerCsrfTokenRequestHandler 将一个 Mono<CsrfToken> 作为名为 org.springframework.security.web.server.csrf.CsrfToken
的 ServerWebExchange
属性暴露出来。在 5.8 版本中,默认实现是 ServerCsrfTokenRequestAttributeHandler
,它只是将 Mono<CsrfToken>
作为一个交换属性提供。
从 6.0 开始,默认实现是 XorServerCsrfTokenRequestAttributeHandler
,它为 BREACH 提供了防护(参见 gh-4001)。
如果你希望禁用 CsrfToken
的 BREACH 保护并恢复到 5.8 版本的默认设置,可以使用以下 Java 配置来配置 ServerCsrfTokenRequestAttributeHandler
:
- Java
- Kotlin
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.csrf(csrf -> csrf
.csrfTokenRequestHandler(new ServerCsrfTokenRequestAttributeHandler())
)
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
csrf {
csrfTokenRequestHandler = ServerCsrfTokenRequestAttributeHandler()
}
}
}
包含 CSRF 令牌
为了使用同步令牌模式来防范CSRF攻击,我们必须在HTTP请求中包含实际的CSRF令牌。它必须被包含在请求的一部分(如表单参数、HTTP头部或其他选项)中,而这一部分不会由浏览器自动包含在HTTP请求中。
如果你的视图技术没有提供一种简单的方式来订阅 Mono<CsrfToken>
,一个常见的模式是使用 Spring 的 @ControllerAdvice
来直接暴露 CsrfToken
。以下示例将 CsrfToken
放置在默认的属性名称(_csrf
)上,该名称由 Spring Security 的 CsrfRequestDataValueProcessor 使用,以自动将 CSRF 令牌作为隐藏输入包含进去:
- Java
- Kotlin
@ControllerAdvice
public class SecurityControllerAdvice {
@ModelAttribute
Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
return csrfToken.doOnSuccess(token -> exchange.getAttributes()
.put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token));
}
}
@ControllerAdvice
class SecurityControllerAdvice {
@ModelAttribute
fun csrfToken(exchange: ServerWebExchange): Mono<CsrfToken> {
val csrfToken: Mono<CsrfToken>? = exchange.getAttribute(CsrfToken::class.java.name)
return csrfToken!!.doOnSuccess { token ->
exchange.attributes[CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME] = token
}
}
}
幸运的是,Thymeleaf 提供了集成,无需任何额外工作即可使用。
表单 URL 编码
要提交一个 HTML 表单,CSRF 令牌必须作为隐藏输入包含在表单中。以下示例展示了渲染后的 HTML 可能的样子:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
接下来,我们讨论将CSRF令牌作为隐藏输入包含在表单中的各种方法。
自动包含CSRF令牌
Spring Security 的 CSRF 支持通过其 CsrfRequestDataValueProcessor 与 Spring 的 RequestDataValueProcessor 集成。为了让 CsrfRequestDataValueProcessor
正常工作,必须订阅 Mono<CsrfToken>
,并且 CsrfToken
必须作为属性公开,该属性需与 DEFAULT_CSRF_ATTR_NAME 匹配。
幸运的是,Thymeleaf 通过与 RequestDataValueProcessor
集成 处理了所有的样板代码,以确保具有不安全 HTTP 方法(POST)的表单自动包含实际的 CSRF 令牌。
CsrfToken 请求属性
如果其他选项无法将实际的CSRF令牌包含在请求中,你可以利用 Mono<CsrfToken>
作为名为 org.springframework.security.web.server.csrf.CsrfToken
的 ServerWebExchange
属性这一事实。
以下 Thymeleaf 示例假设你暴露 CsrfToken
在一个名为 _csrf
的属性上:
<form th:action="@{/logout}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
th:name="${_csrf.parameterName}"
th:value="${_csrf.token}"/>
</form>
Ajax 和 JSON 请求
如果你使用 JSON,你不能在 HTTP 参数中提交 CSRF 令牌。相反,你可以在 HTTP 头中提交该令牌。
在接下来的部分中,我们将讨论在基于 JavaScript 的应用程序中将 CSRF 令牌作为 HTTP 请求头包含在内的各种方法。
自动包含
Meta 标签
将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>
<!-- ... -->
一旦 meta 标签中包含 CSRF 令牌,JavaScript 代码就可以读取这些 meta 标签并将 CSRF 令牌作为头部包含进去。如果你使用 jQuery,你可以用以下代码读取 meta 标签:
var token = $('meta[name="csrf-token"]').attr('content');
$(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);
});
});
以下示例假设你暴露 CsrfToken
在一个名为 _csrf
的属性上。下面的示例使用 Thymeleaf 来实现这一点:
<html>
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
<!-- ... -->
</head>
<!-- ... -->
CSRF 考虑事项
在实现针对CSRF攻击的防护时,有一些特别需要注意的事项。本节讨论了这些注意事项,特别是与WebFlux环境相关的内容。有关更一般的讨论,请参见CSRF注意事项。
登录
你应该为登录请求启用CSRF防护,以防止伪造的登录尝试。Spring Security 的 WebFlux 支持会自动执行此操作。
注销
你应该为注销请求启用 CSRF 防护,以防止伪造注销尝试。默认情况下,Spring Security 的 LogoutWebFilter
仅处理 HTTP POST 请求。这确保了注销需要一个 CSRF 令牌,并且恶意用户不能强行注销你的用户。
最简单的方法是使用表单来登出。如果你真的想要一个链接,可以使用JavaScript让链接执行POST(可能是在一个隐藏的表单上)。对于禁用了JavaScript的浏览器,你可以选择让链接将用户带到一个执行POST操作的登出确认页面。
如果你真的想使用 HTTP GET 进行注销操作,你可以这样做,但请记住,这样做通常是不推荐的。例如,以下 Java 配置在使用任何 HTTP 方法请求 /logout
URL 时执行注销操作:
- Java
- Kotlin
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.logout(logout -> logout.requiresLogout(new PathPatternParserServerWebExchangeMatcher("/logout")))
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
logout {
requiresLogout = PathPatternParserServerWebExchangeMatcher("/logout")
}
}
}
CSRF 和会话超时
默认情况下,Spring Security 会将 CSRF 令牌存储在 WebSession
中。这种安排可能导致会话过期的情况,这意味着没有预期的 CSRF 令牌来进行验证。
我们已经讨论了通用解决方案来处理会话超时。本节讨论与 WebFlux 支持相关的 CSRF 超时的具体情况。
您可以将预期的CSRF令牌存储在cookie中。有关详细信息,请参阅Custom CsrfTokenRepository部分。
多部分(文件上传)
有关使用 Spring 处理多部分表单的更多信息,请参阅 Spring 参考文档中的 多部分数据 部分。
将CSRF令牌放在Body中
我们已经讨论过将CSRF令牌放在主体中的权衡。
在 WebFlux 应用程序中,你可以通过以下配置来实现:
- Java
- Kotlin
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.csrf(csrf -> csrf.tokenFromMultipartDataEnabled(true))
return http.build();
}
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http {
// ...
csrf {
tokenFromMultipartDataEnabled = true
}
}
}
在URL中包含CSRF令牌
<form method="post"
th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}"
enctype="multipart/form-data">
HiddenHttpMethodFilter
我们已经讨论过覆盖 HTTP 方法。
在 Spring WebFlux 应用程序中,通过使用 HiddenHttpMethodFilter 来覆盖 HTTP 方法。