WebFlux环境的跨站请求伪造(CSRF)问题
本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
本节讨论了Spring Security对WebFlux环境的 跨站请求伪造(CSRF)支持。
使用 Spring Security CSRF 保护
使用Spring Security的CSRF保护的步骤概述如下。
使用正确的 HTTP 动词
防范CSRF攻击的第一步是确保你的网站使用正确的HTTP动词。这一点在《安全的 HTTP 方法必须是幂等的》中详细介绍。
配置 CSRF 保护
下一步是在你的应用程序中配置Spring Security的CSRF保护。默认情况下,Spring Security的CSRF保护已经启用,但你可能需要定制配置。接下来的几个小节涵盖了一些常见的自定义配置。
自定义 CsrfTokenRepository
默认情况下,Spring Security通过使用 WebSessionServerCsrfTokenRepository
在 WebSession
中存储预期的CSRF令牌。有时,你可能需要配置一个自定义的 ServerCsrfTokenRepository
。例如,你可能想把 CsrfToken
持久化在一个cookie中,以支持 基于JavaScript的应用程序。
默认情况下,CookieServerCsrfTokenRepository
会写入一个名为 XSRF-TOKEN
的 cookie,并从名为 X-XSRF-TOKEN
的header或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>
作为一个 exchange 属性可用。
从 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 Token
为了使 synchronizer token (同步令牌)pattern 能够抵御CSRF攻击,我们必须在HTTP请求中包含实际的CSRF令牌。它必须包含在请求的一部分(表单参数,HTTP头,或其他选项),而不是由浏览器自动包含在HTTP请求中。
我们已经看到, Mono<CsrfToken>
是作为 ServerWebExchange
属性暴露的。这意味着任何视图技术都可以访问 Mono<CsrfToken>
,将预期的令牌作为表单 或meta 标签公开。
如果你的视图技术没有提供订阅 Mono<CsrfToken>
的简单方法,一个常见的模式是使用Spring的 @ControllerAdvice
来直接暴露 CsrfToken
。下面的例子将 CsrfToken
放在Spring Security的 CsrfRequestDataValueProcessor 使用的默认属性名(_csrf
)上,以自动包括CSRF令牌作为 hidden input。
CsrfToken
as @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令牌必须作为一个 hidden input 包含在表单中。下面的例子显示了渲染后的HTML可能是什么样子。
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
接下来,我们将讨论将CSRF令牌作为一个 hidden input 在表单中的各种方法。
自动包含 CSRF Token
Spring Security的 CSRF 支持通过 CsrfRequestDataValueProcessor
提供与Spring的 RequestDataValueProcessor
的集成。为了让 CsrfRequestDataValueProcessor
发挥作用,必须订阅 Mono<CsrfToken>
,并且 CsrfToken
必须作为符合 DEFAULT_CSRF_ATTR_NAME
的属性公开。
幸运的是,Thymeleaf通过与 RequestDataValueProcessor
集成,确保具有不安全HTTP方法(POST)的表单自动包括实际的CSRF令牌,为你 处理所有的模板。
CsrfToken Request Attribute
如果在请求中包含实际CSRF令牌的其他选项不起作用,你可以利用 Mono<CsrfToken>
作为 ServerWebExchange
属性公开的事实,该属性名为 org.springframework.security.web.server.csrf.CsrfToken
。
下面的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请求头的各种方式。
Meta 标签
在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>
<!-- ... -->
一旦 meta 标签包含CSRF令牌,JavaScript代码就可以读取元标签,并将CSRF令牌作为一个header。如果你使用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 和 Session 过期
默认情况下,Spring Security将CSRF令牌存储在 WebSession
中。这种安排可能会导致会话(Session)过期的情况,这意味着没有预期的CSRF令牌可以验证。
我们已经讨论了会话超时的 一般解决方案。本节将讨论CSRF超时的具体细节,因为它与WebFlux支持有关。
你可以改变预期CSRF令牌的存储方式,将其放在一个cookie中。详情请见 自定义 CsrfTokenRepository 部分。
Multipart (文件上传)
关于在Spring中使用 multipart 表单的更多信息,请参见Spring参考资料中的 Multipart Data 部分。 |
将CSRF令牌放在Body中
我们 已经讨论了 将CSRF标记放在 body 中的利弊。
在一个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 Token
我们 已经讨论了 在URL中放置CSRF令牌的权衡问题。由于 CsrfToken
是作为 ServerHttpRequest
request attribute,公开的,我们可以用它来创建一个带有CSRF标记的 action
。下面是一个使用Thymeleaf的例子。
<form method="post"
th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}"
enctype="multipart/form-data">
HiddenHttpMethodFilter
我们 已经讨论过 重写HTTP方法。
在Spring WebFlux应用程序中,通过使用 HiddenHttpMethodFilter
来重写HTTP方法。