跨站請求偽造 (CSRF)

在終端使用者可以登入的應用中,考慮如何防範跨站請求偽造 (CSRF) 至關重要。

Spring Security 預設針對不安全的 HTTP 方法(例如 POST 請求)提供 CSRF 攻擊防護,因此無需額外的程式碼。你可以使用以下配置顯式指定預設配置

配置 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 防護,請考慮以下用例

瞭解 CSRF 防護的元件

CSRF 防護由多個元件提供,這些元件組合在CsrfFilter

csrf
圖 1. CsrfFilter 元件

CSRF 防護分為兩部分

  1. 透過委託給CsrfTokenRequestHandler 使 CsrfToken 對應用可用。

  2. 確定請求是否需要 CSRF 防護,載入並驗證令牌,以及處理 AccessDeniedException

csrf processing
圖 2. CsrfFilter 處理流程
  • number 1 首先,載入DeferredCsrfToken,它持有對CsrfTokenRepository 的引用,以便稍後(在number 4 中)載入持久化的 CsrfToken

  • number 2 其次,將 Supplier<CsrfToken>(從 DeferredCsrfToken 建立)傳遞給CsrfTokenRequestHandler,後者負責填充請求屬性,以便使 CsrfToken 對應用的其餘部分可用。

  • number 3 接著,主要的 CSRF 防護處理開始,並檢查當前請求是否需要 CSRF 防護。如果不需要,則繼續過濾器鏈,處理結束。

  • number 4 如果需要 CSRF 防護,最終會從 DeferredCsrfToken 中載入持久化的 CsrfToken

  • number 5 繼續,使用CsrfTokenRequestHandler 解析客戶端提供的實際 CSRF 令牌(如果有)。

  • number 6 將實際的 CSRF 令牌與持久化的 CsrfToken 進行比較。如果有效,則繼續過濾器鏈,處理結束。

  • number 7 如果實際的 CSRF 令牌無效(或缺失),則將 AccessDeniedException 傳遞給AccessDeniedHandler,處理結束。

遷移到 Spring Security 6

從 Spring Security 5 遷移到 6 時,有一些更改可能會影響你的應用。以下是 Spring Security 6 中 CSRF 防護方面的一些變化概述

  • 預設情況下,CsrfToken 的載入現在是延遲的,這透過不再需要在每個請求上載入會話來提高效能。

  • 預設情況下,CsrfToken 現在在每個請求中都包含隨機性,以保護 CSRF 令牌免受 BREACH 攻擊。

Spring Security 6 中的更改對單頁應用 (SPA) 需要額外的配置,因此你可能會發現單頁應用部分特別有用。

有關遷移 Spring Security 5 應用的更多資訊,請參閱遷移章節中的利用保護部分。

持久化 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"/>

你可以使用CookieCsrfTokenRepositoryCsrfToken 持久化到 cookie 中,以支援基於 JavaScript 的應用

預設情況下,CookieCsrfTokenRepository 將令牌寫入名為 XSRF-TOKEN 的 cookie,並從名為 X-XSRF-TOKEN 的 HTTP 請求頭或請求引數 _csrf 中讀取。這些預設值來自 Angular 及其前身 AngularJS

有關此主題的最新資訊,請參閱跨站請求偽造 (XSRF) 防護指南和HttpClientXsrfModule

你可以使用以下配置配置 CookieCsrfTokenRepository

示例中明確將 HttpOnly 設定為 false。這對於允許 JavaScript 框架(如 Angular)讀取它至關重要。如果你不需要直接使用 JavaScript 讀取 cookie 的能力,我們建議省略 HttpOnly(改用 new 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 提供 CsrfTokenBREACH 防護。Spring Security 還提供CsrfTokenRequestAttributeHandler 用於選擇退出 BREACH 防護。你也可以指定自己的實現來自定義處理和解析令牌的策略。

使用 XorCsrfTokenRequestAttributeHandler (BREACH)

XorCsrfTokenRequestAttributeHandlerCsrfToken 作為名為 _csrfHttpServletRequest 屬性提供,並額外提供 BREACH 防護。

CsrfToken 也透過名稱 CsrfToken.class.getName() 作為請求屬性提供。此名稱不可配置,但可以使用 XorCsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 更改名稱 _csrf

此實現還從請求中解析令牌值,可以是請求頭(預設是X-CSRF-TOKENX-XSRF-TOKEN 之一)或請求引數(預設是 _csrf)。

BREACH 防護透過將隨機性編碼到 CSRF 令牌值中來提供,以確保返回的 CsrfToken 在每個請求中都發生變化。當令牌稍後被解析為頭值或請求引數時,它會被解碼以獲得原始令牌,然後將其與持久化的 CsrfToken 進行比較。

Spring Security 預設保護 CSRF 令牌免受 BREACH 攻擊,因此無需額外的程式碼。你可以使用以下配置顯式指定預設配置

配置 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

CsrfTokenRequestAttributeHandlerCsrfToken 作為名為 _csrfHttpServletRequest 屬性提供。

CsrfToken 也透過名稱 CsrfToken.class.getName() 作為請求屬性提供。此名稱不可配置,但可以使用 CsrfTokenRequestAttributeHandler#setCsrfRequestAttributeName 更改名稱 _csrf

此實現還從請求中解析令牌值,可以是請求頭(預設是X-CSRF-TOKENX-XSRF-TOKEN 之一)或請求引數(預設是 _csrf)。

CsrfTokenRequestAttributeHandler 的主要用途是選擇退出 CsrfToken 的 BREACH 防護,可以使用以下配置進行配置

選擇退出 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 介面是一個 @FunctionalInterface,可以使用 lambda 表示式來實現以自定義請求處理。你需要實現完整的介面來自定義如何從請求中解析令牌。請參閱配置單頁應用的 CSRF,瞭解使用委託實現處理和解析令牌的自定義策略的示例。

實現 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)發出請求時,需要 CsrfToken。此外,任何將令牌渲染到響應中的請求也需要它,例如包含用於 CSRF 令牌的隱藏 <input><form> 標籤的網頁。

因為 Spring Security 預設也將 CsrfToken 儲存在 HttpSession 中,延遲載入 CSRF 令牌可以透過避免在每個請求上載入會話來提高效能。

如果你想選擇退出延遲載入的令牌,並讓 CsrfToken 在每個請求上都載入,可以使用以下配置實現

選擇退出延遲載入的 CSRF 令牌
  • 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>

透過將 csrfRequestAttributeName 設定為 null,必須首先載入 CsrfToken 以確定要使用的屬性名稱。這會導致 CsrfToken 在每個請求上都載入。

整合 CSRF 防護

為了使同步器令牌模式能夠防禦 CSRF 攻擊,我們必須在 HTTP 請求中包含實際的 CSRF 令牌。這必須包含在請求的某個部分(表單引數、HTTP 頭或其他部分),該部分不會由瀏覽器自動包含在 HTTP 請求中。

以下部分描述了前端或客戶端應用與受 CSRF 保護的後端應用整合的各種方式

HTML 表單

要提交 HTML 表單,CSRF 令牌必須作為隱藏輸入包含在表單中。例如,渲染的 HTML 可能看起來像這樣

HTML 表單中的 CSRF 令牌
<input type="hidden"
	name="_csrf"
	value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>

以下檢視技術會自動將實際的 CSRF 令牌包含在使用不安全 HTTP 方法(例如 POST)的表單中

如果這些選項不可用,你可以利用 CsrfToken 作為名為_csrfHttpServletRequest 屬性公開的事實。以下示例使用 JSP 實現這一點

包含請求屬性的 HTML 表單中的 CSRF 令牌
<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 預設提供對 CsrfTokenBREACH 防護。當將預期的 CSRF 令牌儲存在 cookie 中時,JavaScript 應用只能訪問原始令牌值,而無法訪問編碼後的值。因此需要提供一個自定義的請求處理程式來解析實際的令牌值。

此外,儲存 CSRF 令牌的 cookie 在認證成功和登出成功時會被清除。Spring Security 預設延遲載入新的 CSRF 令牌,需要額外的工作來返回一個新鮮的 cookie。

在認證成功和登出成功後需要重新整理令牌,因為 CsrfAuthenticationStrategyCsrfLogoutHandler 會清除先前的令牌。客戶端應用在沒有獲取新鮮令牌的情況下將無法執行不安全的 HTTP 請求,例如 POST。

為了輕鬆將單頁應用與 Spring Security 整合,可以使用以下配置

配置單頁應用的 CSRF
  • 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 Meta 標籤中的 CSRF 令牌
<html>
<head>
	<meta name="_csrf" content="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
	<meta name="_csrf_header" content="X-CSRF-TOKEN"/>
	<!-- ... -->
</head>
<!-- ... -->
</html>

為了在請求中包含 CSRF 令牌,你可以利用 CsrfToken 作為名為_csrfHttpServletRequest 屬性公開的事實。以下示例使用 JSP 實現這一點

包含請求屬性的 HTML Meta 標籤中的 CSRF 令牌
<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,可以使用以下程式碼實現

在 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);
	});
});

其他 JavaScript 應用

對於 JavaScript 應用來說,另一種選擇是將 CSRF 令牌包含在 HTTP 響應頭中。

實現此目標的一種方法是使用帶有CsrfTokenArgumentResolver@ControllerAdvice。以下是一個適用於應用中所有 controller 端點的 @ControllerAdvice 示例

HTTP 響應頭中的 CSRF 令牌
  • 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)
	}

}

由於此 @ControllerAdvice 適用於應用中的所有端點,它將導致 CSRF 令牌在每個請求上都載入,這在使用 HttpSessionCsrfTokenRepository 時可能會抵消延遲載入令牌的好處。然而,在使用CookieCsrfTokenRepository 時,這通常不是問題。

重要的是要記住,controller 端點和 controller advice 在 Spring Security 過濾器鏈之後呼叫。這意味著只有當請求透過過濾器鏈到達你的應用時,此 @ControllerAdvice 才會應用。請參閱單頁應用的配置,瞭解如何在過濾器鏈中新增過濾器以更早訪問 HttpServletResponse 的示例。

現在,對於 controller advice 適用的任何自定義端點,CSRF 令牌將在響應頭中(預設是X-CSRF-TOKENX-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
    }

}

如果上述端點在向伺服器認證之前是必需的,你可以考慮新增 .requestMatchers("/csrf").permitAll()

應在應用啟動或初始化時(例如載入時)呼叫此端點以獲取 CSRF 令牌,並在認證成功和登出成功後也呼叫。

在認證成功和登出成功後需要重新整理令牌,因為 CsrfAuthenticationStrategyCsrfLogoutHandler 會清除先前的令牌。客戶端應用在沒有獲取新鮮令牌的情況下將無法執行不安全的 HTTP 請求,例如 POST。

獲取 CSRF 令牌後,你需要自行將其作為 HTTP 請求頭(預設是X-CSRF-TOKENX-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 防護,如下所示

測試 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 防護,可以使用以下配置實現

停用 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 時,以下示例會執行退出登入

使用任何 HTTP 方法退出登入
  • 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 令牌以規避此問題。

如果 JavaScript 不可用,以下小節將討論在 Servlet 應用程式中將 CSRF 令牌放在請求體URL中的選項。

更多關於在 Spring 中使用 multipart 表單的資訊可以在 Spring 參考文件的Multipart 解析器小節和MultipartFilter javadoc中找到。

將 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 配置時,MultipartFilter 在 Spring Security 過濾器之前指定,你可以確保 MultipartFilter<filter-mapping> 元素放置在 web.xml 檔案中 springSecurityFilterChain 之前。

在 URL 中包含 CSRF 令牌

如果不允許未經授權的使用者上傳臨時檔案是不可接受的,另一種方法是將 MultipartFilter 放置在 Spring Security 過濾器之後,並將 CSRF 作為查詢引數包含在表單的 action 屬性中。由於 CsrfToken 作為名為 _csrfHttpServletRequest 屬性公開,我們可以使用它來建立一個包含 CSRF 令牌的 action。以下示例使用 JSP 完成此操作

Action 中的 CSRF 令牌
<form method="post"
	action="./upload?${_csrf.parameterName}=${_csrf.token}"
	enctype="multipart/form-data">

HiddenHttpMethodFilter

我們已經討論了將 CSRF 令牌放在請求體中的權衡。

在 Spring 的 Servlet 支援中,覆蓋 HTTP 方法是透過使用HiddenHttpMethodFilter完成的。更多資訊請參閱參考文件的HTTP 方法轉換小節。

延伸閱讀

現在你已經回顧了 CSRF 保護,可以考慮深入瞭解漏洞利用防護,包括安全頭部HTTP 防火牆,或者繼續學習如何測試你的應用程式。