持久化認證

使用者首次請求受保護資源時,會提示他們輸入憑據。提示憑據最常見的方式之一是將使用者重定向到登入頁面。未經認證的使用者請求受保護資源的 HTTP 交換摘要可能如下所示

示例 1. 未經認證的使用者請求受保護資源
GET / HTTP/1.1
Host: example.com
Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b
HTTP/1.1 302 Found
Location: /login

使用者提交其使用者名稱和密碼。

已提交使用者名稱和密碼
POST /login HTTP/1.1
Host: example.com
Cookie: SESSION=91470ce0-3f3c-455b-b7ad-079b02290f7b

username=user&password=password&_csrf=35942e65-a172-4cd4-a1d4-d16a51147b3e

認證使用者後,會將使用者與新的會話 ID 相關聯,以防止會話固定攻擊

認證使用者與新會話相關聯
HTTP/1.1 302 Found
Location: /
Set-Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8; Path=/; HttpOnly; SameSite=Lax

後續請求會包含會話 cookie,該 cookie 用於在會話剩餘期間認證使用者。

認證會話作為憑據提供
GET / HTTP/1.1
Host: example.com
Cookie: SESSION=4c66e474-3f5a-43ed-8e48-cc1d8cb1d1c8

SecurityContextRepository

在 Spring Security 中,使用者與後續請求的關聯是使用SecurityContextRepository進行的。SecurityContextRepository 的預設實現是DelegatingSecurityContextRepository,它委託給以下實現

HttpSessionSecurityContextRepository

HttpSessionSecurityContextRepositorySecurityContextHttpSession相關聯。如果使用者希望以其他方式或完全不將使用者與後續請求相關聯,則可以用 SecurityContextRepository 的其他實現替換 HttpSessionSecurityContextRepository

NullSecurityContextRepository

如果不需要將 SecurityContextHttpSession 相關聯(例如在使用 OAuth 進行認證時),則NullSecurityContextRepository是 SecurityContextRepository 的一種實現,它不做任何事情。

RequestAttributeSecurityContextRepository

RequestAttributeSecurityContextRepositorySecurityContext 儲存為請求屬性,以確保 SecurityContext 在可能清除 SecurityContext 的跨分派型別的單個請求中可用。

例如,假設客戶端發出請求,被認證,然後發生錯誤。根據 servlet 容器的實現,該錯誤意味著已建立的任何 SecurityContext 都將被清除,然後進行錯誤分派。進行錯誤分派時,沒有建立 SecurityContext。這意味著錯誤頁面無法使用 SecurityContext 進行授權或顯示當前使用者,除非 SecurityContext 以某種方式持久化。

使用 RequestAttributeSecurityContextRepository
  • Java

  • XML

public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		// ...
		.securityContext((securityContext) -> securityContext
			.securityContextRepository(new RequestAttributeSecurityContextRepository())
		);
	return http.build();
}
<http security-context-repository-ref="contextRepository">
	<!-- ... -->
</http>
<b:bean name="contextRepository"
	class="org.springframework.security.web.context.RequestAttributeSecurityContextRepository" />

DelegatingSecurityContextRepository

DelegatingSecurityContextRepositorySecurityContext 儲存到多個 SecurityContextRepository 委託中,並允許按指定順序從任何委託中檢索。

最常見的配置方式如以下示例所示,它允許同時使用RequestAttributeSecurityContextRepositoryHttpSessionSecurityContextRepository

配置 DelegatingSecurityContextRepository
  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		// ...
		.securityContext((securityContext) -> securityContext
			.securityContextRepository(new DelegatingSecurityContextRepository(
				new RequestAttributeSecurityContextRepository(),
				new HttpSessionSecurityContextRepository()
			))
		);
	return http.build();
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
	http {
		// ...
		securityContext {
			securityContextRepository = DelegatingSecurityContextRepository(
				RequestAttributeSecurityContextRepository(),
				HttpSessionSecurityContextRepository()
			)
		}
	}
	return http.build()
}
<http security-context-repository-ref="contextRepository">
	<!-- ... -->
</http>
<bean name="contextRepository"
	class="org.springframework.security.web.context.DelegatingSecurityContextRepository">
		<constructor-arg>
			<bean class="org.springframework.security.web.context.RequestAttributeSecurityContextRepository" />
		</constructor-arg>
		<constructor-arg>
			<bean class="org.springframework.security.web.context.HttpSessionSecurityContextRepository" />
		</constructor-arg>
</bean>

在 Spring Security 6 中,上面顯示的示例是預設配置。

SecurityContextPersistenceFilter

SecurityContextPersistenceFilter負責使用SecurityContextRepository在請求之間持久化 SecurityContext

securitycontextpersistencefilter

number 1 在執行應用程式的其餘部分之前,SecurityContextPersistenceFilterSecurityContextRepository 載入 SecurityContext 並將其設定到 SecurityContextHolder 上。

number 2 接下來,執行應用程式。

number 3 最後,如果 SecurityContext 發生了變化,我們會使用 SecurityContextRepository 儲存 SecurityContext。這意味著在使用 SecurityContextPersistenceFilter 時,只需設定 SecurityContextHolder 即可確保 SecurityContext 使用 SecurityContextRepository 進行持久化。

在某些情況下,響應在 SecurityContextPersistenceFilter 方法完成之前就已經提交併寫入客戶端。例如,如果傳送重定向到客戶端,響應會立即寫回客戶端。這意味著在第 3 步中無法建立 HttpSession,因為會話 ID 無法包含在已寫入的響應中。另一種可能發生的情況是,如果客戶端成功認證,響應在 SecurityContextPersistenceFilter 完成之前提交,並且客戶端在 SecurityContextPersistenceFilter 完成之前發出第二個請求。第二個請求中可能存在錯誤的認證資訊。

為了避免這些問題,SecurityContextPersistenceFilter 會包裝 HttpServletRequest 和 HttpServletResponse,以檢測 SecurityContext 是否已更改,如果更改了,則在響應提交之前儲存 SecurityContext。

SecurityContextHolderFilter

SecurityContextHolderFilter負責使用SecurityContextRepository在請求之間載入 SecurityContext

securitycontextholderfilter

number 1 在執行應用程式的其餘部分之前,SecurityContextHolderFilterSecurityContextRepository 載入 SecurityContext 並將其設定到 SecurityContextHolder 上。

number 2 接下來,執行應用程式。

SecurityContextPersistenceFilter不同,SecurityContextHolderFilter 只加載 SecurityContext,它不儲存 SecurityContext。這意味著在使用 SecurityContextHolderFilter 時,需要顯式儲存 SecurityContext

顯式儲存 SecurityContext
  • Java

  • Kotlin

  • XML

public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		// ...
		.securityContext((securityContext) -> securityContext
			.requireExplicitSave(true)
		);
	return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
    http {
        securityContext {
            requireExplicitSave = true
        }
    }
    return http.build()
}
<http security-context-explicit-save="true">
	<!-- ... -->
</http>

在使用此配置時,重要的是任何用 SecurityContext 設定 SecurityContextHolder 的程式碼,如果該 SecurityContext 需要在請求之間持久化,也應將其儲存到 SecurityContextRepository 中。

例如,以下程式碼

使用 SecurityContextPersistenceFilter 設定 SecurityContextHolder
  • Java

  • Kotlin

SecurityContextHolder.setContext(securityContext);
SecurityContextHolder.setContext(securityContext)

應該替換為

使用 SecurityContextHolderFilter 設定 SecurityContextHolder
  • Java

  • Kotlin

SecurityContextHolder.setContext(securityContext);
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse);
SecurityContextHolder.setContext(securityContext)
securityContextRepository.saveContext(securityContext, httpServletRequest, httpServletResponse)