持續驗證

使用者首次請求受保護的資源時,系統會提示輸入憑證。提示輸入憑證最常見的方式之一是將使用者重新導向至登入頁面。未經驗證的使用者請求受保護資源的 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,用於在工作階段的剩餘時間內驗證使用者身分。

已驗證的工作階段作為憑證提供
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 進行驗證時),NullSecurityContextRepositorySecurityContextRepository的實作,它不執行任何操作。

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 在執行應用程式的其餘部分之前,SecurityContextPersistenceFilter會從SecurityContextRepository載入SecurityContext,並將其設定在SecurityContextHolder上。

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

number 3 最後,如果SecurityContext已變更,我們會使用SecurityContextPersistenceRepository儲存SecurityContext。這表示當使用SecurityContextPersistenceFilter時,只需設定SecurityContextHolder即可確保使用SecurityContextRepository持續保存SecurityContext

在某些情況下,回應會在SecurityContextPersistenceFilter方法完成之前提交並寫入用戶端。例如,如果將重新導向傳送至用戶端,則回應會立即寫回用戶端。這表示步驟 3 中建立HttpSession是不可能的,因為工作階段 ID 無法包含在已寫入的回應中。可能發生的另一種情況是,如果用戶端成功驗證身分,則回應會在SecurityContextPersistenceFilter完成之前提交,並且用戶端在SecurityContextPersistenceFilter完成之前發出第二個請求,則第二個請求中可能會出現錯誤的驗證。

為了避免這些問題,SecurityContextPersistenceFilter會包裝HttpServletRequestHttpServletResponse,以偵測SecurityContext是否已變更,如果已變更,則在回應提交之前儲存SecurityContext

SecurityContextHolderFilter

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

securitycontextholderfilter

number 1 在執行應用程式的其餘部分之前,SecurityContextHolderFilter會從SecurityContextRepository載入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,也應將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)