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
-
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()
}
}
}
前面的示例顯式設定了 |
停用 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>
作為 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 可能是什麼樣子
<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.CsrfToken
的 ServerWebExchange
屬性的事實。
以下 Thymeleaf 示例假設您將 CsrfToken
暴露 在名為 _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 請求頭部包含的各種方法。
Meta 標籤
另一種 將 CSRF 暴露在 cookie 中 的模式是將 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 標籤
$(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 配置會在使用任何 HTTP 方法請求 /logout
URL 時執行退出登入
-
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 應用中,您可以使用以下配置來實現
-
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
}
}
}
HiddenHttpMethodFilter
我們已經 討論過 覆蓋 HTTP 方法。
在 Spring WebFlux 應用中,覆蓋 HTTP 方法是透過使用 HiddenHttpMethodFilter
完成的。