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`

将 CSRF 令牌存储在 Cookie 中
  • 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()
        }
    }
}

前面的示例显式设置了 `cookieHttpOnly=false`。这是必要的,以便让 JavaScript(在本例中为 AngularJS)读取它。如果您不需要直接使用 JavaScript 读取 cookie 的功能,我们建议省略 `cookieHttpOnly=false`(改为使用 `new CookieServerCsrfTokenRepository()`),以提高安全性。

禁用 CSRF 保护

默认情况下,CSRF 保护已启用。但是,如果您的应用程序有意义,您可以禁用 CSRF 保护。

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

禁用 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

禁用BREACH保护
  • 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可能是什么样子。

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.CsrfTokenServerWebExchange属性公开这一事实。

以下Thymeleaf示例假设您CsrfToken公开为名为_csrf的属性

表单中使用请求属性的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 Security将预期的CSRF令牌存储在cookie中。通过将预期的CSRF存储在cookie中,JavaScript框架(例如AngularJS)会自动将实际的CSRF令牌包含在HTTP请求头中。

Meta标签

替代在cookie中公开CSRF的模式是在您的meta标签中包含CSRF令牌。HTML可能如下所示:

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标签:

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

以下示例假设您CsrfToken公开为名为_csrf的属性。以下示例使用Thymeleaf执行此操作:

CSRF meta标签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注意事项

登录

您应该要求登录请求使用CSRF,以防止伪造登录尝试。Spring Security的WebFlux支持会自动执行此操作。

注销

您应该要求注销请求使用CSRF,以防止伪造注销尝试。默认情况下,Spring Security的LogoutWebFilter只处理HTTP POST请求。这确保注销需要CSRF令牌,并且恶意用户无法强制注销您的用户。

最简单的方法是使用表单注销。如果您确实需要链接,您可以使用JavaScript使链接执行POST操作(可能在隐藏表单上)。对于禁用JavaScript的浏览器,您可以选择让链接将用户带到执行POST操作的注销确认页面。

如果您确实想要使用HTTP GET进行注销,您可以这样做,但请记住,这样做通常不建议。例如,以下Java配置在使用任何HTTP方法请求/logout URL时注销:

使用HTTP GET注销
  • 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部分。

多部分(文件上传)

我们已经讨论过如何保护多部分请求(文件上传)免受CSRF攻击会导致先有鸡还是先有蛋的问题。本节讨论如何在WebFlux应用程序中将CSRF令牌放在正文URL中。

有关使用Spring的多部分表单的更多信息,请参见Spring参考中的多部分数据部分。

将CSRF令牌放在正文中

我们已经讨论过将CSRF令牌放在正文中的权衡。

在WebFlux应用程序中,您可以使用以下配置来实现:

启用从multipart/form-data获取CSRF令牌
  • 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令牌

我们已经讨论过将CSRF令牌放在URL中的权衡。由于CsrfToken作为ServerHttpRequest请求属性公开,我们可以使用它来创建一个包含CSRF令牌的action。下面显示了Thymeleaf的示例:

Action中的CSRF令牌
<form method="post"
	th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}"
	enctype="multipart/form-data">

HiddenHttpMethodFilter

我们已经讨论过覆盖HTTP方法。

在Spring WebFlux应用程序中,覆盖HTTP方法是通过使用HiddenHttpMethodFilter来完成的。