身份驗證持久化與 Session 管理
一旦您擁有一個驗證請求的應用程式,重要的是要考慮如何持久化和還原後續請求產生的身份驗證。
預設情況下,這是自動完成的,因此不需要額外的程式碼,但重要的是要知道 HttpSecurity
中的 requireExplicitSave
代表什麼意思。
如果您願意,您可以閱讀更多關於 requireExplicitSave 的作用或 為什麼它很重要。否則,在大多數情況下,您已完成本節。
但在您離開之前,請考慮以下使用案例是否適合您的應用程式
-
我想要直接自行儲存身份驗證,而不是讓 Spring Security 為我執行
-
我正在手動儲存身份驗證,我想要移除它
-
我正在使用
SessionManagementFilter
,我需要關於移轉的指南 -
我想將身份驗證儲存在Session 以外的其他位置
-
我正在使用無狀態身份驗證,但 我仍然想將其儲存在 Session 中
-
我正在使用
SessionCreationPolicy.NEVER
,但應用程式仍在建立 Session。
瞭解 Session 管理的元件
Session 管理支援由幾個協同工作的元件組成,以提供功能。這些元件是:SecurityContextHolderFilter
、SecurityContextPersistenceFilter
和 SessionManagementFilter
。
在 Spring Security 6 中,預設不會設定 |
SessionManagementFilter
SessionManagementFilter
檢查 SecurityContextRepository
的內容與 SecurityContextHolder
的目前內容,以判斷使用者是否在目前的請求期間通過身份驗證,通常是通過非互動式身份驗證機制,例如預先驗證或「記住我」[1]。如果儲存庫包含安全上下文,則篩選器不執行任何操作。如果沒有,且執行緒本機 SecurityContext
包含(非匿名)Authentication
物件,則篩選器會假設它們已由堆疊中先前的篩選器進行身份驗證。然後,它將調用已組態的 SessionAuthenticationStrategy
。
如果目前未驗證使用者身份,則篩選器將檢查是否已請求無效的 Session ID(例如,由於逾時),並將調用已組態的 InvalidSessionStrategy
(如果已設定)。最常見的行為是僅重新導向到固定的 URL,這封裝在標準實作 SimpleRedirectInvalidSessionStrategy
中。當通過命名空間組態無效 Session URL 時,也會使用後者,如先前所述。
移轉離開 SessionManagementFilter
在 Spring Security 5 中,預設組態依賴 SessionManagementFilter
來偵測使用者是否剛通過身份驗證並調用 SessionAuthenticationStrategy
。這樣做的問題在於,這表示在典型的設定中,必須為每個請求讀取 HttpSession
。
在 Spring Security 6 中,預設情況是身份驗證機制本身必須調用 SessionAuthenticationStrategy
。這表示不需要偵測何時完成 Authentication
,因此不需要為每個請求讀取 HttpSession
。
移轉離開 SessionManagementFilter
時需要考量的事項
在 Spring Security 6 中,預設不使用 SessionManagementFilter
,因此,來自 sessionManagement
DSL 的某些方法將不會有任何作用。
方法 | 替代方案 |
---|---|
|
在您的身份驗證機制中組態 |
|
在您的身份驗證機制中組態 |
|
在您的身份驗證機制中組態 |
如果您嘗試使用任何這些方法,將會拋出例外。
自訂儲存身份驗證的位置
預設情況下,Spring Security 會為您將安全上下文儲存在 HTTP Session 中。但是,您可能想要自訂它的原因如下:
-
您可能想要在
HttpSessionSecurityContextRepository
實例上調用個別的 setter -
您可能想要將安全上下文儲存在快取或資料庫中,以啟用水平擴展
首先,您需要建立 SecurityContextRepository
的實作,或使用現有的實作,例如 HttpSessionSecurityContextRepository
,然後您可以在 HttpSecurity
中設定它。
SecurityContextRepository
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
SecurityContextRepository repo = new MyCustomSecurityContextRepository();
http
// ...
.securityContext((context) -> context
.securityContextRepository(repo)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val repo = MyCustomSecurityContextRepository()
http {
// ...
securityContext {
securityContextRepository = repo
}
}
return http.build()
}
<http security-context-repository-ref="repo">
<!-- ... -->
</http>
<bean name="repo" class="com.example.MyCustomSecurityContextRepository" />
上述組態在 |
如果您正在使用自訂身份驗證機制,您可能想要自行儲存 Authentication
。
手動儲存 Authentication
在某些情況下,例如,您可能會手動驗證使用者身份,而不是依賴 Spring Security 篩選器。您可以使用自訂篩選器或 Spring MVC 控制器 端點來執行此操作。如果您想要在請求之間儲存身份驗證(例如,在 HttpSession
中),則必須執行以下操作:
-
Java
private SecurityContextRepository securityContextRepository =
new HttpSessionSecurityContextRepository(); (1)
@PostMapping("/login")
public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) { (2)
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
loginRequest.getUsername(), loginRequest.getPassword()); (3)
Authentication authentication = authenticationManager.authenticate(token); (4)
SecurityContext context = securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication); (5)
securityContextHolderStrategy.setContext(context);
securityContextRepository.saveContext(context, request, response); (6)
}
class LoginRequest {
private String username;
private String password;
// getters and setters
}
1 | 將 SecurityContextRepository 新增至控制器 |
2 | 注入 HttpServletRequest 和 HttpServletResponse 以便能夠儲存 SecurityContext |
3 | 使用提供的憑證建立未驗證的 UsernamePasswordAuthenticationToken |
4 | 調用 AuthenticationManager#authenticate 以驗證使用者身份 |
5 | 建立 SecurityContext 並在其中設定 Authentication |
6 | 將 SecurityContext 儲存在 SecurityContextRepository 中 |
就是這樣。如果您不確定上述範例中的 securityContextHolderStrategy
是什麼,您可以閱讀更多關於 使用 SecurityContextStrategy
區段的資訊。
正確清除身份驗證
如果您正在使用 Spring Security 的 登出支援,那麼它會為您處理許多事情,包括清除和儲存上下文。但是,假設您需要手動將使用者登出您的應用程式。在這種情況下,您需要確保您正確地清除和儲存上下文。
為無狀態身份驗證組態持久性
有時不需要建立和維護 HttpSession
,例如,跨請求持久化身份驗證。某些身份驗證機制(如 HTTP Basic)是無狀態的,因此,會在每個請求上重新驗證使用者身份。
如果您不希望建立 Session,可以使用 SessionCreationPolicy.STATELESS
,如下所示:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
// ...
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
}
return http.build()
}
<http create-session="stateless">
<!-- ... -->
</http>
上述組態正在組態 SecurityContextRepository
以使用 NullSecurityContextRepository
,並且也防止請求被儲存在 Session 中。
如果您正在使用 SessionCreationPolicy.NEVER
,您可能會注意到應用程式仍在建立 HttpSession
。在大多數情況下,發生這種情況是因為請求被儲存在 Session 中,以便在身份驗證成功後,已驗證的資源可以重新請求。為避免這種情況,請參閱如何防止請求被儲存區段。
將無狀態身份驗證儲存在 Session 中
如果由於某種原因,您正在使用無狀態身份驗證機制,但您仍然想要將身份驗證儲存在 Session 中,則可以使用 HttpSessionSecurityContextRepository
而不是 NullSecurityContextRepository
。
對於 HTTP Basic,您可以新增 ObjectPostProcessor
,以變更 BasicAuthenticationFilter
使用的 SecurityContextRepository
HttpSession
中-
Java
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.httpBasic((basic) -> basic
.addObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() {
@Override
public <O extends BasicAuthenticationFilter> O postProcess(O filter) {
filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
return filter;
}
})
);
return http.build();
}
以上內容也適用於其他身份驗證機制,例如 Bearer Token 身份驗證。
瞭解需要明確儲存
在 Spring Security 5 中,預設行為是 SecurityContext
使用 SecurityContextPersistenceFilter
自動儲存到 SecurityContextRepository
。儲存必須在 HttpServletResponse
提交之前且在 SecurityContextPersistenceFilter
之前完成。不幸的是,當在請求完成之前(即在提交 HttpServletResponse
之前)完成時,SecurityContext
的自動持久化可能會讓使用者感到驚訝。也很難追蹤狀態以判斷是否需要儲存,這有時會導致不必要的寫入 SecurityContextRepository
(即 HttpSession
)。
基於這些原因,SecurityContextPersistenceFilter
已被棄用,並由 SecurityContextHolderFilter
取代。在 Spring Security 6 中,預設行為是 SecurityContextHolderFilter
將僅從 SecurityContextRepository
讀取 SecurityContext
,並將其填入 SecurityContextHolder
中。現在,如果使用者希望 SecurityContext
在請求之間持續存在,則必須使用 SecurityContextRepository
明確儲存 SecurityContext
。這消除了歧義,並透過僅在必要時才需要寫入 SecurityContextRepository
(即 HttpSession
)來提高效能。
運作方式
總之,當 requireExplicitSave
為 true
時,Spring Security 會設定 SecurityContextHolderFilter
而不是 SecurityContextPersistenceFilter
組態並行 Session 控制
如果您希望限制單一使用者登入應用程式的能力,Spring Security 透過以下簡單的新增功能提供開箱即用的支援。首先,您需要將以下監聽器新增至您的組態,以讓 Spring Security 保持更新 Session 生命周期事件的資訊:
-
Java
-
Kotlin
-
web.xml
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
open fun httpSessionEventPublisher(): HttpSessionEventPublisher {
return HttpSessionEventPublisher()
}
<listener>
<listener-class>
org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>
然後將以下幾行新增至您的安全性組態:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.maximumSessions(1)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
sessionConcurrency {
maximumSessions = 1
}
}
}
return http.build()
}
<http>
...
<session-management>
<concurrency-control max-sessions="1" />
</session-management>
</http>
這將防止使用者多次登入 - 第二次登入將導致第一次登入失效。
使用 Spring Boot,您可以通過以下方式測試上述組態情境:
-
Java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsTests {
@Autowired
private MockMvc mvc;
@Test
void loginOnSecondLoginThenFirstSessionTerminated() throws Exception {
MvcResult mvcResult = this.mvc.perform(formLogin())
.andExpect(authenticated())
.andReturn();
MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();
this.mvc.perform(get("/").session(firstLoginSession))
.andExpect(authenticated());
this.mvc.perform(formLogin()).andExpect(authenticated());
// first session is terminated by second login
this.mvc.perform(get("/").session(firstLoginSession))
.andExpect(unauthenticated());
}
}
您可以使用 Maximum Sessions 範例 來嘗試。
常見的情況是您希望阻止第二次登入,在這種情況下,您可以使用:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
sessionConcurrency {
maximumSessions = 1
maxSessionsPreventsLogin = true
}
}
}
return http.build()
}
<http>
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>
然後,第二次登入將被拒絕。所謂「拒絕」,是指如果使用基於表單的登入,則使用者將被傳送到 authentication-failure-url
。如果第二次身份驗證通過另一個非互動式機制(例如「記住我」)進行,則會將「未經授權」(401)錯誤傳送到用戶端。如果您希望改用錯誤頁面,則可以在 session-management
元素中新增屬性 session-authentication-error-url
。
使用 Spring Boot,您可以通過以下方式測試上述組態情境:
-
Java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsPreventLoginTests {
@Autowired
private MockMvc mvc;
@Test
void loginOnSecondLoginThenPreventLogin() throws Exception {
MvcResult mvcResult = this.mvc.perform(formLogin())
.andExpect(authenticated())
.andReturn();
MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();
this.mvc.perform(get("/").session(firstLoginSession))
.andExpect(authenticated());
// second login is prevented
this.mvc.perform(formLogin()).andExpect(unauthenticated());
// first session is still valid
this.mvc.perform(get("/").session(firstLoginSession))
.andExpect(authenticated());
}
}
如果您正在為基於表單的登入使用自訂身份驗證篩選器,則必須明確組態並行 Session 控制支援。您可以使用 Maximum Sessions Prevent Login 範例 來嘗試。
偵測逾時
Session 會自行過期,無需執行任何操作來確保安全上下文被移除。也就是說,Spring Security 可以偵測到 Session 何時過期,並採取您指示的特定操作。例如,當使用者使用已過期的 Session 發出請求時,您可能想要重新導向到特定的端點。這是通過 HttpSecurity
中的 invalidSessionUrl
實現的:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.invalidSessionUrl("/invalidSession")
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
invalidSessionUrl = "/invalidSession"
}
}
return http.build()
}
<http>
...
<session-management invalid-session-url="/invalidSession" />
</http>
請注意,如果您使用此機制來偵測 Session 逾時,如果使用者登出然後在不關閉瀏覽器的情況下重新登入,則可能會錯誤地報告錯誤。這是因為當您使 Session 失效時,Session Cookie 不會被清除,即使使用者已登出,也會重新提交。如果是這種情況,您可能想要組態登出以清除 Session Cookie。
自訂無效 Session 策略
invalidSessionUrl
是使用 SimpleRedirectInvalidSessionStrategy
實作設定 InvalidSessionStrategy
的便利方法。如果您想要自訂行為,您可以實作 InvalidSessionStrategy
介面,並使用 invalidSessionStrategy
方法組態它:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.invalidSessionStrategy(new MyCustomInvalidSessionStrategy())
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
invalidSessionStrategy = MyCustomInvalidSessionStrategy()
}
}
return http.build()
}
<http>
...
<session-management invalid-session-strategy-ref="myCustomInvalidSessionStrategy" />
<bean name="myCustomInvalidSessionStrategy" class="com.example.MyCustomInvalidSessionStrategy" />
</http>
在登出時清除 Session Cookie
您可以明確刪除登出時的 JSESSIONID Cookie,例如使用登出處理程式中的 Clear-Site-Data
標頭:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout((logout) -> logout
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
logout {
addLogoutHandler(HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(COOKIES)))
}
}
return http.build()
}
<http>
<logout success-handler-ref="clearSiteDataHandler" />
<b:bean id="clearSiteDataHandler" class="org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler">
<b:constructor-arg>
<b:bean class="org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter">
<b:constructor-arg>
<b:list>
<b:value>COOKIES</b:value>
</b:list>
</b:constructor-arg>
</b:bean>
</b:constructor-arg>
</b:bean>
</http>
這樣做的好處是不受容器限制,並且適用於任何支援 Clear-Site-Data
標頭的容器。
或者,您也可以在登出處理程式中使用以下語法:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout(logout -> logout
.deleteCookies("JSESSIONID")
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
logout {
deleteCookies("JSESSIONID")
}
}
return http.build()
}
<http>
<logout delete-cookies="JSESSIONID" />
</http>
不幸的是,不能保證這在每個 Servlet 容器中都能正常運作,因此您需要在您的環境中進行測試。
如果您在 Proxy 後面執行應用程式,您也可以通過組態 Proxy 伺服器來移除 Session Cookie。例如,通過使用 Apache HTTPD 的 |
<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>
更多關於 Clear Site Data 和 登出區段 的詳細資訊。
瞭解 Session Fixation 攻擊防護
Session fixation 攻擊是一種潛在風險,惡意攻擊者有可能通過存取網站來建立 Session,然後說服另一個使用者使用相同的 Session 登入(例如,通過向他們傳送包含 Session 識別碼作為參數的連結)。Spring Security 通過在使用者登入時建立新的 Session 或以其他方式變更 Session ID 來自動防護此類攻擊。
組態 Session Fixation 防護
您可以通過在三個建議的選項之間進行選擇來控制 Session Fixation 防護的策略:
-
changeSessionId
- 不建立新的 Session。而是使用 Servlet 容器提供的 Session fixation 防護 (HttpServletRequest#changeSessionId()
)。此選項僅在 Servlet 3.1 (Java EE 7) 和更新的容器中可用。在較舊的容器中指定它將導致例外。這是 Servlet 3.1 和更新容器中的預設值。 -
newSession
- 建立新的「乾淨」Session,而不複製現有的 Session 資料(與 Spring Security 相關的屬性仍將被複製)。 -
migrateSession
- 建立新的 Session,並將所有現有的 Session 屬性複製到新的 Session。這是 Servlet 3.0 或更舊容器中的預設值。
您可以通過執行以下操作來組態 Session fixation 防護:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement((session) -> session
.sessionFixation((sessionFixation) -> sessionFixation
.newSession()
)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
sessionFixation {
newSession()
}
}
}
return http.build()
}
<http>
<session-management session-fixation-protection="newSession" />
</http>
當發生 Session fixation 防護時,會在應用程式上下文中發布 SessionFixationProtectionEvent
。如果您使用 changeSessionId
,則此防護也將導致任何 jakarta.servlet.http.HttpSessionIdListener
s 收到通知,因此如果您的程式碼同時監聽這兩種事件,請謹慎使用。
您也可以將 Session fixation 防護設定為 none
以停用它,但不建議這樣做,因為這會讓您的應用程式容易受到攻擊。
使用 SecurityContextHolderStrategy
請考慮以下程式碼區塊:
-
Java
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = this.authenticationManager.authenticate(token);
// ...
SecurityContext context = SecurityContextHolder.createEmptyContext(); (1)
context.setAuthentication(authentication); (2)
SecurityContextHolder.setContext(context); (3)
-
通過靜態存取
SecurityContextHolder
來建立空的SecurityContext
實例。 -
在
SecurityContext
實例中設定Authentication
物件。 -
在
SecurityContextHolder
中靜態設定SecurityContext
實例。
雖然上述程式碼運作良好,但可能會產生一些不良影響:當元件通過 SecurityContextHolder
靜態存取 SecurityContext
時,當有多個想要指定 SecurityContextHolderStrategy
的應用程式上下文時,可能會產生競爭條件。這是因為在 SecurityContextHolder
中,每個類別載入器有一個策略,而不是每個應用程式上下文一個策略。
為了解決這個問題,元件可以從應用程式上下文中連接 SecurityContextHolderStrategy
。預設情況下,它們仍然會從 SecurityContextHolder
查找策略。
這些變更在很大程度上是內部的,但它們為應用程式提供了自動連線 SecurityContextHolderStrategy
的機會,而不是靜態存取 SecurityContext
。若要這樣做,您應該將程式碼變更為以下內容:
-
Java
public class SomeClass {
private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
public void someMethod() {
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = this.authenticationManager.authenticate(token);
// ...
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); (1)
context.setAuthentication(authentication); (2)
this.securityContextHolderStrategy.setContext(context); (3)
}
}
-
使用已組態的
SecurityContextHolderStrategy
建立空的SecurityContext
實例。 -
在
SecurityContext
實例中設定Authentication
物件。 -
在
SecurityContextHolderStrategy
中設定SecurityContext
實例。
強制積極 Session 建立
有時,積極建立 Session 可能很有價值。這可以使用 ForceEagerSessionCreationFilter
來完成,可以使用以下方式組態:
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.ALWAYS
}
}
return http.build()
}
<http create-session="ALWAYS">
</http>
接下來要閱讀的內容
-
使用 Spring Session 的叢集 Session
SessionManagementFilter
偵測到,因為在身份驗證請求期間不會調用篩選器。Session 管理功能必須在這些情況下單獨處理。