跨站請求偽造 (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 防護,請考慮以下用例
-
我想將
CsrfToken
儲存在 cookie 中,而不是會話 (session) 中 -
我想將
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
,處理結束。
持久化 CsrfToken
CsrfToken
使用 CsrfTokenRepository
進行持久化。
預設情況下,使用HttpSessionCsrfTokenRepository
將令牌儲存在會話中。Spring Security 還提供CookieCsrfTokenRepository
將令牌儲存在 cookie 中。你也可以指定自己的實現,將令牌儲存在你喜歡的任何地方。
使用 HttpSessionCsrfTokenRepository
預設情況下,Spring Security 使用HttpSessionCsrfTokenRepository
將預期的 CSRF 令牌儲存在 HttpSession
中,因此無需額外的程式碼。
HttpSessionCsrfTokenRepository
從會話(無論是記憶體、快取還是資料庫)中讀取令牌。如果你需要直接訪問會話屬性,請首先使用 HttpSessionCsrfTokenRepository#setSessionAttributeName
配置會話屬性名稱。
你可以使用以下配置顯式指定預設配置
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) 防護指南和HttpClientXsrfModule。 |
你可以使用以下配置配置 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 令牌可以透過避免在每個請求上載入會話來提高效能。
如果你想選擇退出延遲載入的令牌,並讓 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 中,Angular 等 JavaScript 框架可以自動將實際的 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)
);
return http.build();
}
}
final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler {
private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler();
private final CsrfTokenRequestHandler xor = 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.xor.handle(request, response, csrfToken);
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get();
}
@Override
public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
String headerValue = request.getHeader(csrfToken.getHeaderName());
/*
* 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.
*
* 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 (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken);
}
}
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)
}
}
return http.build()
}
}
class SpaCsrfTokenRequestHandler : CsrfTokenRequestHandler {
private val plain: CsrfTokenRequestHandler = CsrfTokenRequestAttributeHandler()
private val xor: 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.
*/
xor.handle(request, response, csrfToken)
/*
* Render the token value to a cookie by causing the deferred token to be loaded.
*/
csrfToken.get()
}
override fun resolveCsrfTokenValue(request: HttpServletRequest, csrfToken: CsrfToken): String? {
val headerValue = request.getHeader(csrfToken.headerName)
/*
* 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(headerValue)) {
plain
} 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.
*/
xor
}.resolveCsrfTokenValue(request, csrfToken)
}
}
<http>
<!-- ... -->
<csrf
token-repository-ref="tokenRepository" (1)
request-handler-ref="requestHandler"/> (2)
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
<b:bean id="requestHandler"
class="example.SpaCsrfTokenRequestHandler"/>
1 | 配置 CookieCsrfTokenRepository 並將 HttpOnly 設定為 false ,以便 JavaScript 應用可以讀取 cookie。 |
2 | 配置一個自定義的 CsrfTokenRequestHandler ,它根據 CSRF 令牌是 HTTP 請求頭(X-XSRF-TOKEN )還是請求引數(_csrf )來解析令牌。此實現還會導致延遲載入的 CsrfToken 在每個請求上都載入,如果需要,會返回一個新的 cookie。 |
多頁應用
對於 JavaScript 在每個頁面上載入的多頁應用,除了在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>
<!-- ... -->
</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 應用來說,另一種選擇是將 CSRF 令牌包含在 HTTP 響應頭中。
實現此目標的一種方法是使用帶有CsrfTokenArgumentResolver
的 @ControllerAdvice
。以下是一個適用於應用中所有 controller 端點的 @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)
}
}
由於此 |
重要的是要記住,controller 端點和 controller advice 在 Spring Security 過濾器鏈之後呼叫。這意味著只有當請求透過過濾器鏈到達你的應用時,此 |
現在,對於 controller advice
適用的任何自定義端點,CSRF 令牌將在響應頭中(預設是X-CSRF-TOKEN
或X-XSRF-TOKEN
)可用。對後端的任何請求都可以用於從響應中獲取令牌,隨後的請求可以在同名請求頭中包含該令牌。
移動應用
與JavaScript 應用類似,移動應用通常使用 JSON 而不是 HTML。不提供瀏覽器流量的後端應用可以選擇停用 CSRF。在這種情況下,無需額外的操作。
然而,既提供瀏覽器流量又因此仍需要 CSRF 防護的後端應用可能會繼續將 CsrfToken
儲存在會話 (session) 中,而不是cookie 中。
在這種情況下,與後端整合的一種典型模式是公開一個 /csrf
端點,允許前端(移動或瀏覽器客戶端)按需請求 CSRF 令牌。使用此模式的好處是 CSRF 令牌可以繼續延遲載入,並且只在請求需要 CSRF 防護時才需要從會話中載入。使用自定義端點還意味著客戶端應用可以透過發出顯式請求來按需請求生成新的令牌(如果需要)。
此模式可用於需要 CSRF 防護的任何型別應用,不僅限於移動應用。雖然在這些情況下通常不需要此方法,但它是與受 CSRF 保護的後端整合的另一種選擇。 |
以下是使用CsrfTokenArgumentResolver
的 /csrf
端點示例
/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 防護,並配置忽略規則,如下例所示
-
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 和會話超時
預設情況下,Spring Security 使用HttpSessionCsrfTokenRepository
將 CSRF 令牌儲存在 HttpSession
中。這可能導致會話過期,從而沒有可用於驗證的 CSRF 令牌。
我們已經討論了針對會話超時的通用解決方案。本節討論了與 Servlet 支援相關的 CSRF 超時的具體情況。
你可以將 CSRF 令牌的儲存位置更改為 cookie。詳情請參閱使用 CookieCsrfTokenRepository
小節。
如果令牌確實過期了,你可能希望透過指定一個自定義 AccessDeniedHandler
來定製它的處理方式。自定義的 AccessDeniedHandler
可以以你喜歡的任何方式處理 InvalidCsrfTokenException
。
Multipart(檔案上傳)
我們已經討論了如何保護 multipart 請求(檔案上傳)免受 CSRF 攻擊會導致一個先有雞還是先有蛋問題。當 JavaScript 可用時,我們推薦在 HTTP 請求頭中包含 CSRF 令牌以規避此問題。
更多關於在 Spring 中使用 multipart 表單的資訊可以在 Spring 參考文件的Multipart 解析器小節和 |
將 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 過濾器之後,並將 CSRF 作為查詢引數包含在表單的 action 屬性中。由於 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 方法轉換小節。