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