WebFlux 环境下的跨站请求伪造 (CSRF)
本节讨论 Spring Security 对 WebFlux 环境的 跨站请求伪造 (CSRF) 支持。
使用 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>
以将预期令牌公开为 表单 或 元标记。
如果您的视图技术没有提供一种简单的方法来订阅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 示例假设您在名为_csrf
的属性上公开CsrfToken
。
<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 请求标头包含在内的各种方法。
元标签
除了在 cookie 中公开 CSRF 之外,另一种模式是在 meta
标签中包含 CSRF 令牌。HTML 可能看起来像这样
<html>
<head>
<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
<!-- ... -->
</head>
<!-- ... -->
一旦元标签包含 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);
});
});
以下示例假设您公开名为 _csrf
的属性上的 CsrfToken
。以下示例使用 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 部分。
多部分(文件上传)
有关使用 Spring 的多部分表单的更多信息,请参阅 Spring 参考中的 多部分数据 部分。 |
将 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
来完成的。