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通过使用 WebSessionServerCsrfTokenRepositoryWebSession 中存储预期的CSRF令牌。有时,你可能需要配置一个自定义的 ServerCsrfTokenRepository。例如,你可能想把 CsrfToken 持久化在一个cookie中,以支持 基于JavaScript的应用程序

默认情况下,CookieServerCsrfTokenRepository 会写入一个名为 XSRF-TOKEN 的 cookie,并从名为 X-XSRF-TOKEN 的header或HTTP _csrf 参数中读取它。这些默认值来自 AngularJS

你可以在Java配置中配置 CookieServerCsrfTokenRepository

Store CSRF Token in a 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保护。

Disable CSRF Configuration
  • 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 的 CsrfWebFilterServerCsrfTokenRequestHandler 的帮助下,将 Mono<CsrfToken> 作为名为 org.springframework.security.web.server.csrf.CsrfTokenServerWebExchange 属性暴露。在 5.8 中,默认的实现是 ServerCsrfTokenRequestAttributeHandler,它只是让 Mono<CsrfToken> 作为一个 exchange 属性可用。

从 6.0 开始,默认的实现是 XorServerCsrfTokenRequestAttributeHandler,它为 BREACH 提供保护(见 gh-4001)。

如果你希望禁用 CsrfToken 的 BREACH 保护并恢复到 5.8 的默认值,你可以使用以下 Java 配置来配置 ServerCsrfTokenRequestAttributeHandler

Disable BREACH protection
  • 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可能是什么样子。

CSRF Token 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

CSRF Token in Form with Request Attribute
<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中。通过在cookie中存储预期的CSRF,JavaScript框架,如 AngularJS,会自动在HTTP请求头中包含实际的CSRF令牌。

Meta 标签

cookie中暴露CSRF的另一种模式是在你的 meta 标签中包含CSRF令牌。HTML可能看起来像这样。

CSRF meta tag 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 标签。

AJAX send CSRF Token
$(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 tag 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时都会注销。

Log out with 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 和 Session 过期

默认情况下,Spring Security将CSRF令牌存储在 WebSession 中。这种安排可能会导致会话(Session)过期的情况,这意味着没有预期的CSRF令牌可以验证。

我们已经讨论了会话超时的 一般解决方案。本节将讨论CSRF超时的具体细节,因为它与WebFlux支持有关。

你可以改变预期CSRF令牌的存储方式,将其放在一个cookie中。详情请见 自定义 CsrfTokenRepository 部分。

Multipart (文件上传)

我们 已经讨论了 如何保护 multipart 请求(文件上传)不受CSRF攻击,这导致了一个 鸡和蛋 的问题。本节讨论了如何在WebFlux应用程序中实现将CSRF令牌放在bodyurl中。

关于在Spring中使用 multipart 表单的更多信息,请参见Spring参考资料中的 Multipart Data 部分。

将CSRF令牌放在Body中

我们 已经讨论了 将CSRF标记放在 body 中的利弊。

在一个WebFlux应用程序中,你可以通过以下配置来实现。

Enable obtaining CSRF token from multipart/form-data
  • 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的例子。

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

HiddenHttpMethodFilter

我们 已经讨论过 重写HTTP方法。

在Spring WebFlux应用程序中,通过使用 HiddenHttpMethodFilter 来重写HTTP方法。