跨站請求偽造 (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 保護的更多資訊,請考慮以下用例:
-
我想選擇退出延遲令牌
-
我需要整合 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。 -
繼續,客戶端提供的實際 CSRF 令牌(如果有)使用CsrfTokenRequestHandler進行解析。 -
實際的 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。
|
有關此主題的最新資訊,請參閱HttpClient XSRF/CSRF 安全性和withXsrfConfiguration。 |
您可以使用以下配置來配置 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)。
|
透過將隨機性編碼到 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,您可以在 HTTP 請求頭中提交 CSRF 令牌,而不是請求引數。
為了獲取 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.spa());
return http.build();
}
}
import org.springframework.security.config.annotation.web.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
csrf {
spa()
}
}
return http.build()
}
}
<http>
<!-- ... -->
<csrf>
<spa />
</csrf>
</http>
多頁應用程式
對於 JavaScript 在每個頁面上載入的多頁應用程式,除了將 CSRF 令牌儲存在 cookie 中之外,另一種選擇是將其包含在 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 響應頭中。
實現此目的的一種方法是透過使用 @ControllerAdvice 和CsrfTokenArgumentResolver。以下是適用於應用程式中所有控制器端點的 @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 儲存在會話中,而不是儲存在 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
為了處理像 InvalidCsrfTokenException 這樣的 AccessDeniedException,您可以配置 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.config.http.PathPatternRequestMatcherFactoryBean">
<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 保護以防止偽造登入嘗試非常重要要求登入請求進行 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(PathPatternRequestMatcher.withDefaults().matcher("/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 = PathPatternRequestMatcher.withDefaults().matcher("/logout")
}
}
return http.build()
}
}
有關更多資訊,請參閱登出章節。
CSRF 和會話超時
預設情況下,Spring Security 使用HttpSessionCsrfTokenRepository將 CSRF 令牌儲存在 HttpSession 中。這可能導致會話過期,從而沒有 CSRF 令牌可供驗證的情況。
我們已經討論了會話超時的通用解決方案。本節討論 CSRF 超時在 Servlet 支援方面的具體內容。
您可以將 CSRF 令牌的儲存更改為 cookie。有關詳細資訊,請參閱使用 CookieCsrfTokenRepository部分。
如果令牌確實過期了,您可能希望透過指定自定義 AccessDeniedHandler來定製其處理方式。自定義的 AccessDeniedHandler 可以以您喜歡的任何方式處理 InvalidCsrfTokenException。
多部分(檔案上傳)
我們已經討論過如何保護多部分請求(檔案上傳)免受 CSRF 攻擊會導致先有雞還是先有蛋的問題。當 JavaScript 可用時,我們**建議**在 HTTP 請求頭中包含 CSRF 令牌以避免這個問題。
|
您可以在 Spring 參考文件的多部分解析器部分和 |
將 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>
|
為了確保 |
在 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 方法轉換部分找到更多資訊。