跨站請求偽造 (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 繼續,客戶端提供的實際 CSRF 令牌(如果有)使用CsrfTokenRequestHandler進行解析。

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

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

遷移到 Spring Security 6

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

Spring Security 6 中的更改要求單頁應用程式進行額外配置,因此您可能會發現單頁應用程式部分特別有用。

有關遷移 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

有關此主題的最新資訊,請參閱HttpClient XSRF/CSRF 安全性withXsrfConfiguration

您可以使用以下配置來配置 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)。

透過將隨機性編碼到 CSRF 令牌值中以確保返回的 CsrfToken 在每次請求時都會更改,從而提供 BREACH 保護。當令牌隨後被解析為頭部值或請求引數時,它會被解碼以獲取原始令牌,然後將其與持久化的 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,您可以在 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。

在認證成功和登出成功後重新整理令牌是必需的,因為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.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 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 響應頭中。

實現此目的的一種方法是透過使用 @ControllerAdviceCsrfTokenArgumentResolver。以下是適用於應用程式中所有控制器端點的 @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時,這通常不是問題。

需要記住的是,控制器端點和控制器通知在 Spring Security 過濾器鏈**之後**被呼叫。這意味著此 @ControllerAdvice 僅在請求透過過濾器鏈到達您的應用程式時才會被應用。請參閱單頁應用程式的配置,瞭解在過濾器鏈中新增過濾器以更早訪問 HttpServletResponse 的示例。

CSRF 令牌現在將在響應頭中可用(預設為X-CSRF-TOKENX-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
    }

}

如果上述端點需要在與伺服器進行身份驗證之前,您可以考慮新增 .requestMatchers("/csrf").permitAll()

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

在認證成功和登出成功後重新整理令牌是必需的,因為CsrfAuthenticationStrategyCsrfLogoutHandler將清除之前的令牌。客戶端應用程式將無法在未獲得新令牌的情況下執行不安全的 HTTP 請求,例如 POST。

一旦您獲得了 CSRF 令牌,您需要自己將其作為 HTTP 請求頭(預設為X-CSRF-TOKENX-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 保護,如下所示:

測試 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.config.http.PathPatternRequestMatcherFactoryBean">
                <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 保護以防止偽造登入嘗試非常重要要求登入請求進行 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(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 令牌以避免這個問題。

如果 JavaScript 不可用,以下部分討論了在 Servlet 應用程式中將 CSRF 令牌放置在主體URL中的選項。

您可以在 Spring 參考文件的多部分解析器部分和MultipartFilter javadoc中找到有關將多部分表單與 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>

為了確保 MultipartFilter 在 XML 配置中位於 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 防火牆,或者繼續學習如何測試您的應用程式。

© . This site is unofficial and not affiliated with VMware.