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:
- 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)能够读取该 cookie。如果您不需要直接用 JavaScript 读取 cookie 的能力,我们建议省略 cookieHttpOnly=false(通过使用 new CookieServerCsrfTokenRepository() 替代),以提高安全性。
禁用 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> 作为交换属性提供。
自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 令牌
对于同步令牌模式来说,要有效防御 CSRF 攻击,我们必须在 HTTP 请求中包含实际的 CSRF 令牌。该令牌必须包含在请求的某个部分(如表单参数、HTTP 头部或其他选项)中,且该部分不会被浏览器自动包含在 HTTP 请求内。
如果你的视图技术没有提供订阅 Mono<CsrfToken> 的简便方法,一种常见的模式是使用 Spring 的 @ControllerAdvice 直接暴露 CsrfToken。以下示例将 CsrfToken 放置在 Spring Security 的 CsrfRequestDataValueProcessor 所使用的默认属性名称(_csrf)上,以自动将 CSRF 令牌作为隐藏输入包含在内:
- Java
- Kotlin
@ControllerAdvice
public class SecurityControllerAdvice {
@ModelAttribute
Mono<CsrfToken> csrfToken(ServerWebExchange exchange) {
Mono<CsrfToken> csrfToken = exchange.getAttribute(CsrfToken.class.getName());
return csrfToken.doOnSuccess((token) -> token.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 可能呈现的样子:
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
接下来,我们将探讨在表单中以隐藏输入字段形式包含CSRF令牌的多种方法。
自动包含 CSRF 令牌
Spring Security 的 CSRF 支持通过其 CsrfRequestDataValueProcessor 与 Spring 的 RequestDataValueProcessor 进行集成。要使 CsrfRequestDataValueProcessor 正常工作,必须订阅 Mono<CsrfToken>,并且 CsrfToken 必须作为属性暴露,该属性需与 CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME 匹配。
幸运的是,Thymeleaf 为您处理了所有样板代码,它通过与 RequestDataValueProcessor 集成,确保采用不安全 HTTP 方法(如 POST)的表单会自动包含实际的 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的模式是将CSRF令牌包含在meta标签中。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,可以通过以下代码读取元标签:
$(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 配置会在请求 /logout URL 时(无论使用何种 HTTP 方法)执行登出:
- 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 令牌可供验证。
我们已经讨论过通用解决方案来处理会话超时问题。本节将专门讨论 CSRF 超时在 WebFlux 支持中的具体细节。
您可以将预期的CSRF令牌存储方式更改为存储在cookie中。详情请参阅自定义CsrfTokenRepository部分。
多部分(文件上传)
有关在 Spring 中使用多部分表单的更多信息,请参阅 Spring 参考文档中的 Multipart Data 部分。
将 CSRF 令牌置于请求体中
我们已经讨论过将 CSRF 令牌放在请求体中的权衡。
在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令牌
<form method="post"
th:action="@{/upload(${_csrf.parameterName}=${_csrf.token})}"
enctype="multipart/form-data">
HiddenHttpMethodFilter
我们已经讨论过如何重写 HTTP 方法。
在 Spring WebFlux 应用中,通过使用 HiddenHttpMethodFilter 来覆盖 HTTP 方法。