# WebFlux 环境中的跨站点请求伪造

本节讨论 Spring Security 对 WebFlux 环境的[跨站点请求伪造](../../features/exploits/csrf.html#csrf)支持。

## 使用 Spring 安全 CSRF 保护

使用 Spring Security 的 CSRF 保护的步骤概述如下：

* [使用适当的 HTTP 动词](#webflux-csrf-idempotent)

* [配置 CSRF 保护](#webflux-csrf-configure)

* [包括 CSRF 令牌](#webflux-csrf-include)

### 使用适当的 HTTP 动词

防止 CSRF 攻击的第一步是确保你的网站使用正确的 HTTP 动词。这在[安全的方法必须是幂等的。](../../features/exploits/csrf.html#csrf-protection-idempotent)中有详细介绍。

### 配置 CSRF 保护

下一步是在应用程序中配置 Spring Security 的 CSRF 保护。 Spring 默认情况下，Security 的 CSRF 保护是启用的，但你可能需要定制配置。下面是一些常见的定制。

#### 自定义 CSRFTokenRepository

默认情况下， Spring Security 使用`WebSessionServerCsrfTokenRepository`将预期的 CSRF 令牌存储在`WebSession`中。在某些情况下，用户可能希望配置自定义`ServerCsrfTokenRepository`。例如，可能希望将 cookie 中的`CsrfToken`持久化到[支持基于 爪哇Script 的应用程序](#webflux-csrf-include-ajax-auto)。

默认情况下，`CookieServerCsrfTokenRepository`将写到一个名为`XSRF-TOKEN`的 cookie，并从一个名为`X-XSRF-TOKEN`的头部或 HTTP 参数`_csrf`读取它。这些默认值来自[AngularJS](https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection)

你可以在 爪哇 配置中使用以下方法配置`CookieServerCsrfTokenRepository`:

例 1。在 cookie 中存储 CSRF 令牌

爪哇

```
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
	return http.build();
}
```

Kotlin

```
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        csrf {
            csrfTokenRepository = CookieServerCsrfTokenRepository.withHttpOnlyFalse()
        }
    }
}
```

|   |示例显式设置`cookieHttpOnly=false`.<br/>这是允许 爪哇Script（即 Angularjs）读取它所必需的。<br/>如果不需要直接使用 爪哇Script 读取 cookie 的能力，建议省略`cookieHttpOnly=false`（通过使用`new CookieServerCsrfTokenRepository()`代替）以提高安全性。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 禁用 CSRF 保护

默认情况下启用了 CSRF 保护。但是，如果 CSRF 保护[对你的应用程序来说是有意义的](../../features/exploits/csrf.html#csrf-when)，则禁用 CSRF 保护非常简单。

下面的 Java 配置将禁用 CSRF 保护。

例 2。禁用 CSRF 配置

Java

```
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.csrf(csrf -> csrf.disable()))
	return http.build();
}
```

Kotlin

```
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        csrf {
            disable()
        }
    }
}
```

### 包括 CSRF 令牌

为了使[同步器令牌模式](../../features/exploits/csrf.html#csrf-protection-stp)能够抵御 CSRF 攻击，我们必须在 HTTP 请求中包含实际的 CSRF 令牌。这必须包含在请求的一部分（即表单参数、HTTP 头等）中，而该部分不是由浏览器自动包含在 HTTP 请求中的。

Spring Security 的[CSRFWebfilter ](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/CsrfWebFilter.html)将[Mono\<CsrfToken\>](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfToken.html)公开为名为`ServerWebExchange`的属性。这意味着，任何视图技术都可以访问`Mono<CsrfToken>`以将预期的令牌公开为[form](#webflux-csrf-include-form-attr)或[meta tag](#webflux-csrf-include-ajax-meta)。

如果你的视图技术没有提供订阅`Mono<CsrfToken>`的简单方法，那么一个常见的模式是使用 Spring 的`@ControllerAdvice`直接公开`CsrfToken`。例如，以下代码将在 Spring Security 的[CsrfrequestDataValueProcessor ](#webflux-csrf-include-form-auto)使用的默认属性名（`_csrf`）上放置`CsrfToken`，以自动将 CSRF 令牌作为隐藏输入。

例 3。`CsrfToken`as`@ModelAttribute`

Java

```
@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));
	}
}
```

Kotlin

```
@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 提供了[整合](#webflux-csrf-include-form-auto)，它的工作不需要任何额外的工作。

#### 表单 URL 编码

为了发布 HTML 表单，CSRF 令牌必须作为隐藏输入包含在表单中。例如，呈现的 HTML 可能看起来像：

例 4。CSRF 令牌 HTML

```
<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
```

接下来，我们将讨论将 CSRF 令牌以一种形式包含为隐藏输入的各种方法。

##### CSRF 令牌自动包含

Spring Security 的 CSRF 支持通过其[CsrfrequestDataValueProcessor ](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.html)提供与 Spring 的[RequestDataValueProcessor ](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/result/view/RequestDataValueProcessor.html)的集成。为了使`CsrfRequestDataValueProcessor`工作，必须订阅`Mono<CsrfToken>`，并且`CsrfToken`必须是与[作为属性公开](#webflux-csrf-include-subscribe)匹配的[默认 \_CSRF\_ATTR\_Name ](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.html#DEFAULT_CSRF_ATTR_NAME)。

幸运的是，ThymeLeaf[提供支持](https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#integration-with-requestdatavalueprocessor)通过与`RequestDataValueProcessor`集成来为你处理所有的样板文件，以确保具有不安全的 HTTP 方法（即 POST）的窗体将自动包括实际的 CSRF 令牌。

##### CSRFToken 请求属性

如果用于在请求中包含实际 CSRF 令牌的[其他选择](#webflux-csrf-include)不起作用，则可以利用以下事实：`Mono<CsrfToken>`[is exposed](#webflux-csrf-include)作为`ServerWebExchange`属性，该属性名为`org.springframework.security.web.server.csrf.CsrfToken`。

下面的 ThymeLeaf 示例假设你在一个名为`_csrf`的属性上[expose](#webflux-csrf-include-subscribe)。

例 5。具有请求属性的表单中的 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 请求头包含在内的各种方法。

##### 自动包含

Spring 安全性可以很容易地[configured](#webflux-csrf-configure-custom-repository)将预期的 CSRF 令牌存储在 cookie 中。通过将预期的 CSRF 存储在 Cookie 中，像[AngularJS](https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection)这样的 JavaScript 框架将自动在 HTTP 请求头中包含实际的 CSRF 令牌。

##### 元标签

[在 cookie 中暴露 CSRF ](#webflux-csrf-include-form-auto)的另一种模式是在`meta`标记中包含 CSRF 标记。HTML 可能看起来是这样的：

例 6。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，可以通过以下方式完成此操作：

例 7。Ajax 发送 CSRF 令牌

```
$(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 进行此操作的示例如下所示：

例 8。CSRF 元标记 JSP

```
<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 考虑因素](../../features/exploits/csrf.html#csrf-considerations)。

### 登录

这是重要的[需要 CSRF 才能登录](../../features/exploits/csrf.html#csrf-considerations-login)请求，以防止伪造日志的企图。 Spring Security 的 WebFlux 支持提供了开箱即用的服务。

### 注销

重要的是[需要 CSRF 才能注销](../../features/exploits/csrf.html#csrf-considerations-logout)请求，以防止伪造注销尝试。默认情况下 Spring Security 的`LogoutWebFilter`只处理 HTTP POST 请求。这确保了注销需要 CSRF 令牌，并且恶意用户不能强制注销你的用户。

最简单的方法是使用表单注销。如果你真的想要一个链接，可以使用 JavaScript 让该链接执行一个 POST（例如，可能在一个隐藏的表单上）。对于禁用了 JavaScript 的浏览器，你可以选择让链接将用户带到将执行 POST 的注销确认页面。

如果你真的想使用 HTTP GET 与注销，你可以这样做，但请记住，这通常是不推荐的。例如，下面的 Java 配置将使用任何 HTTP 方法请求的 URL`/logout`执行注销：

例 9。用 HTTP GET 登出

Java

```
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.logout(logout -> logout.requiresLogout(new PathPatternParserServerWebExchangeMatcher("/logout")))
	return http.build();
}
```

Kotlin

```
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
        // ...
        logout {
            requiresLogout = PathPatternParserServerWebExchangeMatcher("/logout")
        }
    }
}
```

### CSRF 和会话暂停

默认情况下， Spring Security 将 CSRF 令牌存储在`WebSession`中。这可能导致会话过期的情况，这意味着没有预期的 CSRF 令牌来验证。

我们已经讨论了[一般解决方案](../../features/exploits/csrf.html#csrf-considerations-login)到会话的超时。本节讨论 CSRF 超时的细节，因为它与 WebFlux 支持有关。

将预期的 CSRF 令牌的存储更改为 cookie 中的存储是很简单的。有关详细信息，请参阅[自定义 CSRFTokenRepository ](#webflux-csrf-configure-custom-repository)部分。

### 

我们有[已经讨论过了](../../features/exploits/csrf.html#csrf-considerations-multipart)如何保护多部分请求（文件上传）不受 CSRF 攻击导致[鸡和蛋](https://en.wikipedia.org/wiki/Chicken_or_the_egg)问题。本节讨论如何在 WebFlux 应用程序中实现将 CSRF 令牌放置在[body](#webflux-csrf-considerations-multipart-body)和[url](#webflux-csrf-considerations-multipart-url)中。

|   |关于使用具有 Spring 的多部分表单的更多信息可以在 Spring 引用的[多部分数据](https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web-reactive.html#webflux-multipart)部分中找到。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 将 CSRF 标记放入体内

我们有[已经讨论过了](../../features/exploits/csrf.html#csrf-considerations-multipart)在主体中放置 CSRF 标记的权衡。

在 WebFlux 应用程序中，可以使用以下配置对其进行配置：

例 10。启用从多部分/表单数据获取 CSRF 令牌

Java

```
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
	http
		// ...
		.csrf(csrf -> csrf.tokenFromMultipartDataEnabled(true))
	return http.build();
}
```

Kotlin

```
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    return http {
		// ...
        csrf {
            tokenFromMultipartDataEnabled = true
        }
    }
}
```

#### 在 URL 中包含 CSRF 令牌

我们有[已经讨论过了](../../features/exploits/csrf.html#csrf-considerations-multipart)在 URL 中放置 CSRF 标记的权衡。由于`CsrfToken`被公开为`ServerHttpRequest`[请求属性](#webflux-csrf-include)，因此我们可以使用它来创建带有 CSRF 令牌的`action`。ThymeLeaf 的一个示例如下所示：

例 11。CSRF 令牌正在运行

```
<form method="post"
	th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}"
	enctype="multipart/form-data">
```

### HiddenHttpMethodFilter

我们已经[已经讨论过了](../../features/exploits/csrf.html#csrf-considerations-override-method)覆盖了 HTTP 方法。

在 Spring WebFlux 应用程序中，使用[HiddenHttpMethodFilter ](https://docs.spring.io/spring-framework/docs/5.2.x/javadoc-api/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.html)重写 HTTP 方法。