WebFlux 环境下的跨站请求伪造 (CSRF)

本节讨论 Spring Security 对 跨站请求伪造 (CSRF) 在 WebFlux 环境下的支持。

使用 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.CsrfTokenServerWebExchange 属性暴露出来。在 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 中,诸如 AngularJS 等 JavaScript 框架会自动将实际的 CSRF 令牌包含在 HTTP 请求头部中。

Meta 标签

另一种 将 CSRF 暴露在 cookie 中 的模式是将 CSRF 令牌包含在您的 meta 标签中。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 一节。

Multipart (文件上传)

我们已经 讨论过 如何保护 multipart 请求(文件上传)免受 CSRF 攻击会产生一个 先有鸡还是先有蛋 的问题。本节讨论如何在 WebFlux 应用中实现将 CSRF 令牌放置在 请求体URL 中。

有关在 Spring 中使用 multipart 表单的更多信息,请参见 Spring 参考文档的 Multipart Data 一节。

将 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 完成的。