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方法。