WebFlux 环境下的跨站请求伪造 (CSRF)
本节讨论 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()
}
}
}
前面的示例显式设置了 |
禁用 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>
作为 ServerWebExchange
属性暴露出来。这意味着任何视图技术都可以访问 Mono<CsrfToken>
,将预期的令牌暴露为 表单 或 meta 标签。
如果您的视图技术没有提供订阅 Mono<CsrfToken>
的简单方法,一个常见的模式是使用 Spring 的 @ControllerAdvice
直接暴露 CsrfToken
。下面的示例将 CsrfToken
放在 Spring Security 的 CsrfRequestDataValueProcessor 用于自动将 CSRF 令牌作为隐藏输入包含的默认属性名称 (_csrf
) 上
CsrfToken
作为 @ModelAttribute
-
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 标签
$(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。详情请参见 自定义 CsrfTokenRepository 一节。
Multipart (文件上传)
我们已经 讨论过 如何保护 multipart 请求(文件上传)免受 CSRF 攻击会产生一个 先有鸡还是先有蛋 的问题。本节讨论如何在 WebFlux 应用中实现将 CSRF 令牌放置在 请求体 和 URL 中。
有关在 Spring 中使用 multipart 表单的更多信息,请参见 Spring 参考文档的 Multipart Data 一节。 |
将 CSRF 令牌放入请求体
我们已经 讨论过 将 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
}
}
}
HiddenHttpMethodFilter
我们已经 讨论过 覆盖 HTTP 方法。
在 Spring WebFlux 应用中,覆盖 HTTP 方法是通过使用 HiddenHttpMethodFilter
完成的。