跨站請求偽造 (CSRF)
在終端使用者可以登入的應用程式中,考量如何防範跨站請求偽造 (CSRF) 非常重要。
Spring Security 預設針對不安全的 HTTP 方法(例如 POST 請求)提供 CSRF 攻擊防護,因此無需額外程式碼。您可以使用以下方式明確指定預設組態
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf(Customizer.withDefaults());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf { }
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf/>
</http>
若要進一步瞭解應用程式的 CSRF 防護,請考量以下使用案例
-
我需要將應用程式從 Spring Security 5 遷移至 6
-
我想將
CsrfToken
儲存在自訂位置 -
我想選擇停用延遲令牌
-
我需要將 Thymeleaf、JSP 或其他檢視技術與後端整合的指南
-
我需要將 Angular 或其他 JavaScript 框架與後端整合的指南
-
我需要關於處理錯誤的指南
-
我需要關於停用 CSRF 防護的指南
瞭解 CSRF 防護的組件
CSRF 防護由多個組件提供,這些組件組成於 CsrfFilter
中

CsrfFilter
組件CSRF 防護分為兩個部分
-
透過委派給
CsrfTokenRequestHandler
,使應用程式可以使用CsrfToken
。 -
判斷請求是否需要 CSRF 防護,載入並驗證令牌,以及處理
AccessDeniedException
。

CsrfFilter
處理流程-
首先,載入
DeferredCsrfToken
,其中包含對CsrfTokenRepository
的參考,以便稍後載入持久化的CsrfToken
(在中)。
-
其次,將
Supplier<CsrfToken>
(從DeferredCsrfToken
建立)提供給CsrfTokenRequestHandler
,後者負責填充請求屬性,使CsrfToken
可供應用程式的其餘部分使用。 -
第三,主要的 CSRF 防護處理開始,並檢查目前請求是否需要 CSRF 防護。如果不需要,則繼續過濾器鏈並結束處理。
-
如果需要 CSRF 防護,則最終從
DeferredCsrfToken
載入持久化的CsrfToken
。 -
繼續,使用
CsrfTokenRequestHandler
解析用戶端提供的實際 CSRF 令牌(如果有的話)。 -
將實際 CSRF 令牌與持久化的
CsrfToken
進行比較。如果有效,則繼續過濾器鏈並結束處理。 -
如果實際 CSRF 令牌無效(或遺失),則將
AccessDeniedException
傳遞給AccessDeniedHandler
並結束處理。
遷移至 Spring Security 6
從 Spring Security 5 遷移至 6 時,有一些變更可能會影響您的應用程式。以下概述 Spring Security 6 中 CSRF 防護方面的變更
-
預設情況下,
CsrfToken
的載入現在是延遲的,以提高效能,不再需要在每個請求上載入 Session。
Spring Security 6 中的變更需要針對單頁應用程式進行額外組態,因此您可能會發現「單頁應用程式」章節特別有用。 |
持久化 CsrfToken
CsrfToken
使用 CsrfTokenRepository
持久化。
預設情況下,HttpSessionCsrfTokenRepository
用於將令牌儲存在 Session 中。Spring Security 也提供 CookieCsrfTokenRepository
,用於將令牌儲存在 Cookie 中。您也可以指定自己的實作,將令牌儲存在您喜歡的任何位置。
使用 HttpSessionCsrfTokenRepository
預設情況下,Spring Security 使用 HttpSessionCsrfTokenRepository
將預期的 CSRF 令牌儲存在 HttpSession
中,因此無需額外程式碼。
HttpSessionCsrfTokenRepository
從 Session 讀取令牌(無論是在記憶體中、快取中或資料庫中)。如果您需要直接存取 Session 屬性,請先使用 HttpSessionCsrfTokenRepository#setSessionAttributeName
設定 Session 屬性名稱。
您可以使用以下組態明確指定預設組態
HttpSessionCsrfTokenRepository
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(new HttpSessionCsrfTokenRepository())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = HttpSessionCsrfTokenRepository()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository"/>
使用 CookieCsrfTokenRepository
您可以使用 CookieCsrfTokenRepository
將 CsrfToken
持久化到 Cookie 中,以支援基於 JavaScript 的應用程式。
CookieCsrfTokenRepository
預設寫入名為 XSRF-TOKEN
的 Cookie,並從名為 X-XSRF-TOKEN
的 HTTP 請求標頭或請求參數 _csrf
中讀取。這些預設值來自 Angular 及其前身 AngularJS。
如需關於此主題的最新資訊,請參閱 「跨站請求偽造 (XSRF) 防護」指南和 |
您可以使用以下組態設定 CookieCsrfTokenRepository
此範例明確將 |
自訂 CsrfTokenRepository
在某些情況下,您可能想要實作自訂的 CsrfTokenRepository
。
一旦您實作了 CsrfTokenRepository
介面,您可以使用以下組態設定 Spring Security 以使用它
CsrfTokenRepository
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(new CustomCsrfTokenRepository())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = CustomCsrfTokenRepository()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="example.CustomCsrfTokenRepository"/>
處理 CsrfToken
CsrfToken
使用 CsrfTokenRequestHandler
提供給應用程式。此組件也負責從 HTTP 標頭或請求參數解析 CsrfToken
。
預設情況下,XorCsrfTokenRequestAttributeHandler
用於提供 CsrfToken
的 BREACH 防護。Spring Security 也提供 CsrfTokenRequestAttributeHandler
,用於選擇停用 BREACH 防護。您也可以指定自己的實作,以自訂處理和解析令牌的策略。
使用 XorCsrfTokenRequestAttributeHandler
(BREACH)
XorCsrfTokenRequestAttributeHandler
使 CsrfToken
可作為名為 _csrf
的 HttpServletRequest
屬性使用,並額外提供 BREACH 防護。
|
此實作也會從請求中解析令牌值,可以是請求標頭(預設為 X-CSRF-TOKEN
或 X-XSRF-TOKEN
其中之一)或請求參數(預設為 _csrf
)。
BREACH 防護透過將隨機性編碼到 CSRF 令牌值中來提供,以確保傳回的 |
Spring Security 預設保護 CSRF 令牌免受 BREACH 攻擊,因此無需額外程式碼。您可以使用以下組態明確指定預設組態
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler"/>
使用 CsrfTokenRequestAttributeHandler
CsrfTokenRequestAttributeHandler
使 CsrfToken
可作為名為 _csrf
的 HttpServletRequest
屬性使用。
|
此實作也會從請求中解析令牌值,可以是請求標頭(預設為 X-CSRF-TOKEN
或 X-XSRF-TOKEN
其中之一)或請求參數(預設為 _csrf
)。
CsrfTokenRequestAttributeHandler
的主要用途是選擇停用 CsrfToken
的 BREACH 防護,可以使用以下組態進行設定
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler"/>
自訂 CsrfTokenRequestHandler
您可以實作 CsrfTokenRequestHandler
介面來自訂處理和解析令牌的策略。
|
一旦您實作了 CsrfTokenRequestHandler
介面,您可以使用以下組態設定 Spring Security 以使用它
CsrfTokenRequestHandler
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(new CustomCsrfTokenRequestHandler())
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRequestHandler = CustomCsrfTokenRequestHandler()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="example.CustomCsrfTokenRequestHandler"/>
延遲載入 CsrfToken
預設情況下,Spring Security 會延遲載入 CsrfToken
,直到需要時才載入。
當使用不安全的 HTTP 方法(例如 POST)發出請求時,需要 |
由於 Spring Security 也預設將 CsrfToken
儲存在 HttpSession
中,因此延遲 CSRF 令牌可以提高效能,而無需在每個請求上載入 Session。
如果您想要選擇停用延遲令牌並導致在每個請求上載入 CsrfToken
,您可以使用以下組態來執行此操作
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler();
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null);
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRequestHandler(requestHandler)
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
val requestHandler = XorCsrfTokenRequestAttributeHandler()
// set the name of the attribute the CsrfToken will be populated on
requestHandler.setCsrfRequestAttributeName(null)
http {
// ...
csrf {
csrfTokenRequestHandler = requestHandler
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-handler-ref="requestHandler"/>
</http>
<b:bean id="requestHandler"
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler">
<b:property name="csrfRequestAttributeName">
<b:null/>
</b:property>
</b:bean>
透過將 |
與 CSRF 防護整合
為了使用同步器令牌模式來防範 CSRF 攻擊,我們必須在 HTTP 請求中包含實際的 CSRF 令牌。這必須包含在請求的一部分中(表單參數、HTTP 標頭或其他部分),瀏覽器不會自動將其包含在 HTTP 請求中。
以下各節說明前端或用戶端應用程式與受 CSRF 保護的後端應用程式整合的各種方式
HTML 表單
若要提交 HTML 表單,CSRF 令牌必須作為隱藏輸入包含在表單中。例如,呈現的 HTML 可能如下所示
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
以下檢視技術會自動將實際 CSRF 令牌包含在具有不安全 HTTP 方法(例如 POST)的表單中
-
任何其他與
RequestDataValueProcessor
整合的檢視技術(透過CsrfRequestDataValueProcessor
) -
您也可以透過
csrfInput
標籤自行包含令牌
如果這些選項不可用,您可以利用 CsrfToken
作為名為 _csrf
的 HttpServletRequest
屬性公開的事實。以下範例使用 JSP 執行此操作
<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form>
JavaScript 應用程式
JavaScript 應用程式通常使用 JSON 而不是 HTML。如果您使用 JSON,您可以將 CSRF 令牌提交在 HTTP 請求標頭中,而不是請求參數中。
為了取得 CSRF 令牌,您可以設定 Spring Security 將預期的 CSRF 令牌儲存在 Cookie 中。透過將預期的令牌儲存在 Cookie 中,JavaScript 框架(例如 Angular)可以自動將實際 CSRF 令牌作為 HTTP 請求標頭包含在內。
將單頁應用程式 (SPA) 與 Spring Security 的 CSRF 防護整合時,BREACH 防護和延遲令牌有一些特殊考量。完整組態範例在下一節中提供。 |
您可以在以下各節中閱讀關於不同類型的 JavaScript 應用程式
單頁應用程式
將單頁應用程式 (SPA) 與 Spring Security 的 CSRF 防護整合時,有一些特殊考量。
回想一下,Spring Security 預設提供 CsrfToken
的 BREACH 防護。當將預期的 CSRF 令牌儲存在 Cookie 中時,JavaScript 應用程式只能存取純令牌值,而無法存取編碼值。將需要提供用於解析實際令牌值的自訂請求處理常式。
此外,儲存 CSRF 令牌的 Cookie 將在身份驗證成功和登出成功後清除。Spring Security 預設會延遲載入新的 CSRF 令牌,並且需要額外的工作才能傳回新的 Cookie。
在身份驗證成功和登出成功後重新整理令牌是必要的,因為 |
為了輕鬆地將單頁應用程式與 Spring Security 整合,可以使用以下組態
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) (1)
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) (2)
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); (3)
return http.build();
}
}
final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
this.delegate.handle(request, response, csrfToken);
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*/
if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
return super.resolveCsrfTokenValue(request, csrfToken);
}
/*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
return this.delegate.resolveCsrfTokenValue(request, csrfToken);
}
}
final class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.getToken();
filterChain.doFilter(request, response);
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse() (1)
csrfTokenRequestHandler = SpaCsrfTokenRequestHandler() (2)
}
}
http.addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java) (3)
return http.build()
}
}
class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() {
private val delegate: CsrfTokenRequestHandler = XorCsrfTokenRequestAttributeHandler()
override fun handle(request: HttpServletRequest, response: HttpServletResponse, csrfToken: Supplier<CsrfToken>) {
/*
* Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
* the CsrfToken when it is rendered in the response body.
*/
delegate.handle(request, response, csrfToken)
}
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
/*
* If the request contains a request header, use CsrfTokenRequestAttributeHandler
* to resolve the CsrfToken. This applies when a single-page application includes
* the header value automatically, which was obtained via a cookie containing the
* raw CsrfToken.
*/
return if (StringUtils.hasText(request.getHeader(csrfToken.headerName))) {
super.resolveCsrfTokenValue(request, csrfToken)
} else {
/*
* In all other cases (e.g. if the request contains a request parameter), use
* XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
* when a server-side rendered form includes the _csrf request parameter as a
* hidden input.
*/
delegate.resolveCsrfTokenValue(request, csrfToken)
}
}
}
class CsrfCookieFilter : OncePerRequestFilter() {
@Throws(ServletException::class, IOException::class)
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
val csrfToken = request.getAttribute("_csrf") as CsrfToken
// Render the token value to a cookie by causing the deferred token to be loaded
csrfToken.token
filterChain.doFilter(request, response)
}
}
<http>
<!-- ... -->
<csrf
token-repository-ref="tokenRepository" (1)
request-handler-ref="requestHandler"/> (2)
<custom-filter ref="csrfCookieFilter" after="BASIC_AUTH_FILTER"/> (3)
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
class="example.SpaCsrfTokenRequestHandler"/>
<b:bean id="csrfCookieFilter"
class="example.CsrfCookieFilter"/>
1 | 設定 CookieCsrfTokenRepository ,並將 HttpOnly 設定為 false ,以便 JavaScript 應用程式可以讀取 Cookie。 |
2 | 設定自訂 CsrfTokenRequestHandler ,根據 CSRF 令牌是 HTTP 請求標頭 (X-XSRF-TOKEN ) 還是請求參數 (_csrf ) 來解析 CSRF 令牌。 |
3 | 設定自訂 Filter 以在每個請求上載入 CsrfToken ,如果需要,這將傳回新的 Cookie。 |
多頁應用程式
對於 JavaScript 在每個頁面上載入的多頁應用程式,將 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>
<!-- ... -->
</html>
為了在請求中包含 CSRF 令牌,您可以利用 CsrfToken
作為名為 _csrf
的 HttpServletRequest
屬性公開的事實。以下範例使用 JSP 執行此操作
<html>
<head>
<meta name="_csrf" content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<!-- ... -->
</head>
<!-- ... -->
</html>
一旦 Meta 標籤包含 CSRF 令牌,JavaScript 程式碼就可以讀取 Meta 標籤並將 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);
});
});
其他 JavaScript 應用程式
JavaScript 應用程式的另一個選項是在 HTTP 回應標頭中包含 CSRF 令牌。
實現此目的的一種方法是使用 CsrfTokenArgumentResolver
的 @ControllerAdvice
。以下是 @ControllerAdvice
的範例,它適用於應用程式中的所有控制器端點
-
Java
-
Kotlin
@ControllerAdvice
public class CsrfControllerAdvice {
@ModelAttribute
public void getCsrfToken(HttpServletResponse response, CsrfToken csrfToken) {
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
}
}
@ControllerAdvice
class CsrfControllerAdvice {
@ModelAttribute
fun getCsrfToken(response: HttpServletResponse, csrfToken: CsrfToken) {
response.setHeader(csrfToken.headerName, csrfToken.token)
}
}
由於此 |
重要的是要記住,控制器端點和控制器建議是在 Spring Security 過濾器鏈之後呼叫的。這表示只有在請求通過過濾器鏈傳遞到您的應用程式時,才會套用此 |
現在,對於控制器建議套用的任何自訂端點,CSRF 令牌都將在回應標頭中可用(預設為 X-CSRF-TOKEN
或 X-XSRF-TOKEN
)。可以使用對後端的任何請求從回應中取得令牌,而後續請求可以在具有相同名稱的請求標頭中包含令牌。
行動應用程式
與 JavaScript 應用程式類似,行動應用程式通常使用 JSON 而不是 HTML。不處理瀏覽器流量的後端應用程式可以選擇停用 CSRF。在這種情況下,無需額外工作。
但是,也處理瀏覽器流量並因此仍然需要 CSRF 防護的後端應用程式可以繼續將 CsrfToken
儲存在 Session 中,而不是 Cookie 中。
在此案例中,與後端整合的典型模式是公開一個 /csrf
端點,以允許前端(行動裝置或瀏覽器用戶端)按需請求 CSRF 令牌。使用此模式的好處是,CSRF 令牌可以繼續延遲,並且僅在請求需要 CSRF 保護時才需要從 session 中載入。使用自訂端點也表示用戶端應用程式可以透過發出明確的請求,按需請求產生新的令牌(如有必要)。
此模式可用於任何需要 CSRF 保護的應用程式類型,而不僅僅是行動應用程式。雖然在這些情況下通常不需要此方法,但這是與受 CSRF 保護的後端整合的另一種選擇。 |
以下是 /csrf
端點的範例,該端點使用了 CsrfTokenArgumentResolver
/csrf
端點-
Java
-
Kotlin
@RestController
public class CsrfController {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken csrfToken) {
return csrfToken;
}
}
@RestController
class CsrfController {
@GetMapping("/csrf")
fun csrf(csrfToken: CsrfToken): CsrfToken {
return csrfToken
}
}
如果上述端點需要在與伺服器進行身份驗證之前使用,您可以考慮新增 |
應在應用程式啟動或初始化時(例如,在載入時),以及在身份驗證成功和登出成功後呼叫此端點以獲取 CSRF 令牌。
在身份驗證成功和登出成功後重新整理令牌是必要的,因為 |
一旦您獲得 CSRF 令牌,您將需要將其作為 HTTP 請求標頭(預設為 X-CSRF-TOKEN
或 X-XSRF-TOKEN
之一)自行包含進去。
處理 AccessDeniedException
為了處理 AccessDeniedException
,例如 InvalidCsrfTokenException
,您可以配置 Spring Security 以您喜歡的任何方式處理這些例外。例如,您可以使用以下配置來配置自訂的拒絕存取頁面
AccessDeniedHandler
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling((exceptionHandling) -> exceptionHandling
.accessDeniedPage("/access-denied")
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
exceptionHandling {
accessDeniedPage = "/access-denied"
}
}
return http.build()
}
}
<http>
<!-- ... -->
<access-denied-handler error-page="/access-denied"/>
</http>
CSRF 測試
您可以使用 Spring Security 的 測試支援 和 CsrfRequestPostProcessor
來測試 CSRF 保護,如下所示
-
Java
-
Kotlin
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfTests {
private MockMvc mockMvc;
@BeforeEach
public void setUp(WebApplicationContext applicationContext) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply(springSecurity())
.build();
}
@Test
public void loginWhenValidCsrfTokenThenSuccess() throws Exception {
this.mockMvc.perform(post("/login").with(csrf())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, "/"));
}
@Test
public void loginWhenInvalidCsrfTokenThenForbidden() throws Exception {
this.mockMvc.perform(post("/login").with(csrf().useInvalidToken())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden());
}
@Test
public void loginWhenMissingCsrfTokenThenForbidden() throws Exception {
this.mockMvc.perform(post("/login")
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser
public void logoutWhenValidCsrfTokenThenSuccess() throws Exception {
this.mockMvc.perform(post("/logout").with(csrf())
.accept(MediaType.TEXT_HTML))
.andExpect(status().is3xxRedirection())
.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"));
}
}
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [SecurityConfig::class])
@WebAppConfiguration
class CsrfTests {
private lateinit var mockMvc: MockMvc
@BeforeEach
fun setUp(applicationContext: WebApplicationContext) {
mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext)
.apply<DefaultMockMvcBuilder>(springSecurity())
.build()
}
@Test
fun loginWhenValidCsrfTokenThenSuccess() {
mockMvc.perform(post("/login").with(csrf())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().is3xxRedirection)
.andExpect(header().string(HttpHeaders.LOCATION, "/"))
}
@Test
fun loginWhenInvalidCsrfTokenThenForbidden() {
mockMvc.perform(post("/login").with(csrf().useInvalidToken())
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden)
}
@Test
fun loginWhenMissingCsrfTokenThenForbidden() {
mockMvc.perform(post("/login")
.accept(MediaType.TEXT_HTML)
.param("username", "user")
.param("password", "password"))
.andExpect(status().isForbidden)
}
@Test
@WithMockUser
@Throws(Exception::class)
fun logoutWhenValidCsrfTokenThenSuccess() {
mockMvc.perform(post("/logout").with(csrf())
.accept(MediaType.TEXT_HTML))
.andExpect(status().is3xxRedirection)
.andExpect(header().string(HttpHeaders.LOCATION, "/login?logout"))
}
}
停用 CSRF 保護
預設情況下,CSRF 保護是啟用的,這會影響與後端整合和測試您的應用程式。在停用 CSRF 保護之前,請考慮它是否對您的應用程式有意義。
您也可以考慮是否只有某些端點不需要 CSRF 保護,並配置忽略規則,如下例所示
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.ignoringRequestMatchers("/api/*")
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
ignoringRequestMatchers("/api/*")
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf request-matcher-ref="csrfMatcher"/>
</http>
<b:bean id="csrfMatcher"
class="org.springframework.security.web.util.matcher.AndRequestMatcher">
<b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
<b:constructor-arg>
<b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
<b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
<b:constructor-arg value="/api/*"/>
</b:bean>
</b:bean>
</b:constructor-arg>
</b:bean>
如果您需要停用 CSRF 保護,您可以使用以下配置來執行此操作
-
Java
-
Kotlin
-
XML
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf.disable());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
disable()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf disabled="true"/>
</http>
CSRF 考量
在實作 CSRF 攻擊防護時,有一些特殊的考量。本節討論與 Servlet 環境相關的考量。有關更一般的討論,請參閱CSRF 考量。
登入
對於登入請求需要 CSRF 以防止偽造登入嘗試,這一點非常重要。Spring Security 的 Servlet 支援開箱即用即可做到這一點。
登出
對於登出請求需要 CSRF 以防止偽造登出嘗試,這一點非常重要。如果 CSRF 保護已啟用(預設值),Spring Security 的 LogoutFilter
將僅處理 HTTP POST 請求。這確保了登出需要 CSRF 令牌,並且惡意使用者無法強行讓您的使用者登出。
最簡單的方法是使用表單讓使用者登出。如果您真的想要使用連結,您可以使用 JavaScript 讓連結執行 POST(可能在隱藏表單上)。對於禁用 JavaScript 的瀏覽器,您可以選擇讓連結將使用者帶到執行 POST 的登出確認頁面。
如果您真的想將 HTTP GET 用於登出,您可以這樣做。但是,請記住,這通常不建議使用。例如,以下程式碼會在以任何 HTTP 方法請求 /logout
URL 時登出
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.logout((logout) -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
);
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
logout {
logoutRequestMatcher = AntPathRequestMatcher("/logout")
}
}
return http.build()
}
}
有關更多資訊,請參閱登出章節。
CSRF 和 Session 超時
預設情況下,Spring Security 使用 HttpSessionCsrfTokenRepository
將 CSRF 令牌儲存在 HttpSession
中。這可能會導致 session 過期的情況,從而沒有 CSRF 令牌可用於驗證。
我們已經討論過 session 超時的一般解決方案。本節討論與 Servlet 支援相關的 CSRF 超時的具體細節。
您可以將 CSRF 令牌的儲存變更為 Cookie。有關詳細資訊,請參閱使用 CookieCsrfTokenRepository
章節。
如果令牌確實過期,您可能希望透過指定自訂 AccessDeniedHandler
來自訂其處理方式。自訂 AccessDeniedHandler
可以以您喜歡的任何方式處理 InvalidCsrfTokenException
。
Multipart(檔案上傳)
我們已經討論過如何保護 multipart 請求(檔案上傳)免受 CSRF 攻擊會導致雞生蛋蛋生雞的問題。當 JavaScript 可用時,我們建議在 HTTP 請求標頭中包含 CSRF 令牌以規避此問題。
您可以在 Spring 參考文件的 Multipart Resolver 章節和 |
將 CSRF 令牌放置在請求主體中
我們已經討論過將 CSRF 令牌放置在請求主體中的權衡。在本節中,我們將討論如何配置 Spring Security 以從請求主體中讀取 CSRF。
為了從請求主體中讀取 CSRF 令牌,MultipartFilter
在 Spring Security 過濾器之前指定。在 Spring Security 過濾器之前指定 MultipartFilter
意味著調用 MultipartFilter
沒有授權,這意味著任何人都可以將臨時檔案放置在您的伺服器上。但是,只有授權使用者才能提交由您的應用程式處理的檔案。一般來說,這是建議的方法,因為臨時檔案上傳對大多數伺服器的影響應該可以忽略不計。
MultipartFilter
-
Java
-
Kotlin
-
XML
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
class SecurityApplicationInitializer : AbstractSecurityWebApplicationInitializer() {
override fun beforeSpringSecurityFilterChain(servletContext: ServletContext?) {
insertFilters(servletContext, MultipartFilter())
}
}
<filter>
<filter-name>MultipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>MultipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
為了確保使用 XML 配置時 |
在 URL 中包含 CSRF 令牌
如果讓未經授權的使用者上傳臨時檔案是不可接受的,另一種選擇是將 MultipartFilter
放置在 Spring Security 過濾器之後,並在表單的 action 屬性中包含作為查詢參數的 CSRF。由於 CsrfToken
作為名為 _csrf
的 HttpServletRequest
屬性公開,我們可以使用它來建立帶有 CSRF 令牌的 action
。以下範例使用 JSP 執行此操作
<form method="post"
action="./upload?${_csrf.parameterName}=${_csrf.token}"
enctype="multipart/form-data">
HiddenHttpMethodFilter
我們已經討論過將 CSRF 令牌放置在請求主體中的權衡。
在 Spring 的 Servlet 支援中,覆寫 HTTP 方法是透過使用 HiddenHttpMethodFilter
完成的。您可以在參考文件的 HTTP 方法轉換 章節中找到更多資訊。