Authentication Persistence and Session Management
一旦您擁有了正在對請求進行身份驗證的應用程序,就必須考慮如何在將來的請求中持久化和恢復結果身份驗證。
默認情況下,這是自動完成的,因此不需要額外的代碼,盡管了解 requireExplicitSave在 HttpSecurity 中的含義非常重要。
如果您愿意,您可以關于 RequureExplicSave 正在做什么 requireExplicitSave is doing 或者為什么它很重要why it’s important的內容。否則,在大多數情況下您將完成本節。
但是在離開之前,考慮一下這些用例是否適合您的應用程序:
- 我想了解會話管理的組成部分(I want to Understand Session Management’s components)
- 我想限制用戶可以同時登錄的次數(I want to restrict the number of times a user can be logged in concurrently)
- 我想自己直接存儲身份驗證,而不是由 Spring Security 代勞(I want to store the authentication directly myself instead of Spring Security doing it for me)
- 我正在手動存儲身份驗證,我想刪除它(I am storing the authentication manually and I want to remove it)
- 我正在使用 SessionManagementFilter,我需要遠離它的指導(I am using
SessionManagementFilter
and I need guidance on moving away from that) - 我希望將身份驗證存儲在會話之外的其他內容中(I want to store the authentication in something other than the session)
- 我正在使用無狀態身份驗證,但是我仍然希望將其存儲在會話中(I am using a stateless authentication, but I’d still like to store it in the session)
- 我正在使用 SessionCreationPolicy.Never,但是應用程序仍然在創建會話。(I am using
SessionCreationPolicy.NEVER
but the application is still creating sessions.)
Understanding Session Management’s Components
會話管理支持由幾個組件組成,它們共同提供功能。
這些組件是 SecurityContextHolderFilter
、 SecurityContextPersistenceFilter
和 SessionManagementFilter
。
在 SpringSecurity6中,默認情況下不設置 SecurityContextPersisenceFilter 和 SessionManagementFilter。除此之外,任何應用程序都應該只設置 SecurityContextHolderFilter 或 SecurityContextPersisenceFilter,而不能同時設置兩者。
The SessionManagementFilter
SessionManagementFilter 根據 SecurityContextHolder 的當前內容檢查 SecurityContextRepository 的內容,以確定用戶在當前請求期間是否已被身份驗證,通常是通過非交互式身份驗證機制,如預先身份驗證或 remember-me [1]。如果存儲庫包含安全上下文,則filter不執行任何操作。如果沒有,并且線程本地的 SecurityContext 包含一個(非匿名的) Authentication 對象,則篩選器假定它們已經通過堆棧中以前的篩選器進行了身份驗證。然后它將調用配置的 SessionAuthenticationStrategy。
如果用戶當前沒有經過身份驗證,filter 將檢查是否請求了無效的會話 ID (例如,由于超時) ,并在設置了一個會話 ID 的情況下調用配置的 InvalidSessionStrategy。最常見的行為就是重定向到一個固定的 URL,這封裝在標準實現 SimpleRedirectInvalidSessionStrategy 中。如前所述,在通過命名空間配置無效的會話 URL 時也使用后者。
Moving Away From SessionManagementFilter
在 Spring Security 5中,默認配置依賴于 SessionManagementFilter 來檢測用戶是否剛剛通過身份驗證并調用 SessionAuthenticationStrategy。這樣做的問題在于,它意味著在典型的設置中,必須為每個請求讀取 HttpSession。
在 SpringSecurity6中,默認情況是身份驗證機制本身必須調用 SessionAuthenticationStrategy。這意味著不需要檢測身份驗證何時完成,因此不需要為每個請求讀取 HttpSession。
Things To Consider When Moving Away From SessionManagementFilter
在 Spring Security 6中,默認情況下不使用 SessionManagementFilter,因此,來自 sessionManagement DSL 的一些方法不會產生任何效果。
Method | Replacement |
---|---|
sessionAuthenticationErrorUrl | Configure an AuthenticationFailureHandler in your authentication mechanism |
sessionAuthenticationFailureHandler | Configure an AuthenticationFailureHandler in your authentication mechanism |
sessionAuthenticationStrategy | Configure an SessionAuthenticationStrategy in your authentication mechanism as discussed above |
如果嘗試使用這些方法中的任何一種,將引發異常。
Customizing Where the Authentication Is Stored
默認情況下,SpringSecurity 在 HTTP 會話中為您存儲安全上下文。然而,這里有幾個你可能需要自定義的原因:
- 您可能希望在 HttpSessionSecurityContextRepository 實例上調用個別的setters
- 您可能希望將安全上下文存儲在緩存或數據庫中,以啟用水平伸縮
首先,您需要創建 SecurityContextRepository 的實現,或者使用類似 HttpSessionSecurityContextRepository 的現有實現,然后您可以在 HttpSecurity 中設置它。
Customizing the SecurityContextRepository
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {SecurityContextRepository repo = new MyCustomSecurityContextRepository();http// ....securityContext((context) -> context.securityContextRepository(repo));return http.build();
}
上述配置設置 SecurityContextHolderFilter 上的 SecurityContextRepository 和參與身份驗證過濾器,如 UsernamePasswordAuthenticationFilter。要在無狀態篩選器中設置它,請參閱如何自定義 SecurityContextRepository for Statless Authentication。
如果使用自定義身份驗證機制,則可能希望自己存儲身份驗證。
Storing the Authentication
manually
例如,在某些情況下,您可能需要手動驗證用戶,而不是依賴于 Spring Security filters。您可以使用自定義過濾器或 Spring MVC 控制器端點來完成這項工作。如果要在請求之間保存身份驗證,例如,在 HttpSession 中,必須這樣做:
private SecurityContextRepository securityContextRepository =new HttpSessionSecurityContextRepository(); // 1@PostMapping("/login")
public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) { // 2UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.getUsername(), loginRequest.getPassword()); // 3 Authentication authentication = authenticationManager.authenticate(token); // 4 SecurityContext context = securityContextHolderStrategy.createEmptyContext();context.setAuthentication(authentication); // 5securityContextHolderStrategy.setContext(context);securityContextRepository.saveContext(context, request, response); // 6
}class LoginRequest {private String username;private String password;// getters and setters
}
- 將 SecurityContextRepository 添加到控制器
- 注入 HttpServletRequest 和 HttpServletResponse 以保存 SecurityContext
- 使用提供的憑據創建未經身份驗證的 UsernamePasswordAuthenticationToken
- 調用 AuthenticationManager # authenticate 對用戶進行身份驗證
- 創建 SecurityContext 并在其中設置身份驗證
- 在 SecurityContextRepository 中保存 SecurityContext
就是這樣。如果您不確定上面示例中的 securityContextHolderStrategy 是什么,可以在使用 SecurityContextStrategy 部分了解更多信息。
Properly Clearing an Authentication
如果您正在使用 Spring Security 的 Logout Support,那么它將為您處理許多事情,包括清除和保存上下文。但是,假設您需要手動將用戶從應用程序中注銷。在這種情況下,您需要確保正確地清除和保存上下文。
Configuring Persistence for Stateless Authentication
例如,有時不需要創建和維護 HttpSession,以便跨請求持久化身份驗證。某些身份驗證機制(如 HTTPBasic)是無狀態的,因此會在每個請求上重新驗證用戶。
如果您不希望創建會話,可以使用 SessionCreationPolicy. STATELSS,如下所示:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http// ....sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));return http.build();
}
上述配置正在將 SecurityContextRepository 配置為使用 NullSecurityContextRepository,并且還阻止將請求保存到會話中。
如果您使用的是 SessionCreationPolicy. NEVER,您可能會注意到應用程序仍然在創建 HttpSession。在大多數情況下,發生這種情況是因為請求被保存在會話中,以便在身份驗證成功后再次請求經過身份驗證的資源。為了避免這種情況,請參考如何防止請求被保存 how to prevent the request of being saved 節。
Storing Stateless Authentication in the Session
如果出于某種原因,您正在使用無狀態身份驗證機制,但仍然希望在會話中存儲身份驗證,則可以使用 HttpSessionSecurityContextRepository 而不是 NullSecurityContextRepository。
對于 HTTP Basic,可以添加一個 ObjectPostProcessor,用于更改 BasicAuthenticationFilter 使用的 SecurityContextRepository:
Store HTTP Basic authentication in the HttpSession
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {http// ....httpBasic((basic) -> basic.addObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() {@Overridepublic <O extends BasicAuthenticationFilter> O postProcess(O filter) {filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());return filter;}}));return http.build();
}
上述方法也適用于其他身份驗證機制,如承載令牌身份驗證。
在 Spring Security 5中,默認行為是使用 SecurityContextPersisenceFilter 將 SecurityContext 自動保存到 SecurityContextRepository。必須在提交 HttpServletResponse 之前和 SecurityContextPersisenceFilter 之前保存。不幸的是,SecurityContext 的自動持久化在請求完成之前(即在提交 HttpServletResponse 之前)完成時可能會讓用戶感到驚訝。跟蹤狀態以確定是否需要保存,從而導致有時不必要地寫入 SecurityContextRepository (即 HttpSession)也很復雜。
由于這些原因,不推薦使用 SecurityContextHolderFilter 替換 SecurityContextPersisenceFilter。在 Spring Security 6中,默認行為是 SecurityContextHolderFilter 將只從 SecurityContextRepository 讀取 SecurityContext 并在 SecurityContextHolder 中填充它。用戶現在必須使用 SecurityContextRepository 顯式地保存 SecurityContext,如果他們希望 SecurityContext 在請求之間保持的話。這樣可以消除模糊性,并在必要時只需要寫入 SecurityContextRepository (即 HttpSession) ,從而提高性能。
How it works
總而言之,如果 requireExplicitSave 為 true,Spring Security 將設置 SecurityContextHolderFilter 而不是 SecurityContextPersisenceFilter
Configuring Concurrent Session Control
如果您希望限制單個用戶登錄到您的應用程序的能力,Spring Security 通過以下簡單的添加支持開箱即用。首先,您需要將以下偵聽器添加到您的配置中,以保持 Spring Security 對會話生命周期事件的更新:
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher();
}
然后在安全配置中添加以下代碼行:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http.sessionManagement(session -> session.maximumSessions(1));return http.build();
}
這將阻止用戶多次登錄——第二次登錄將導致第一次登錄失效。
使用 Spring Boot,您可以通過以下方式測試上面的配置場景:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsTests {@Autowiredprivate MockMvc mvc;@Testvoid 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 loginthis.mvc.perform(get("/").session(firstLoginSession)).andExpect(unauthenticated());}}
可以使用“最大會話”示例進行嘗試。
另外,通常您希望防止第二次登錄,在這種情況下,您可以使用:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http.sessionManagement(session -> session.maximumSessions(1).maxSessionsPreventsLogin(true));return http.build();
}
第二次登錄將被拒絕。“拒絕”的意思是,如果使用的是基于表單的登錄,那么用戶將被發送到身份驗證-失敗-url。如果第二次身份驗證是通過另一種非交互機制進行的,比如“ remember-me”,那么將向客戶端發送一個“未授權”(401)錯誤。如果希望使用錯誤頁面,可以將屬性 session-entication-error-url 添加到session-management元素中。
使用 Spring Boot,您可以通過以下方式測試上述配置:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsPreventLoginTests {@Autowiredprivate MockMvc mvc;@Testvoid 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 preventedthis.mvc.perform(formLogin()).andExpect(unauthenticated());// first session is still validthis.mvc.perform(get("/").session(firstLoginSession)).andExpect(authenticated());}}
如果對基于表單的登錄使用自定義身份驗證filter,則必須顯式配置并發會話控制支持。您可以使用“最大會話防止登錄”示例嘗試使用它。
Detecting Timeouts
會話會自行到期,不需要做任何事情來確保刪除安全上下文。也就是說,SpringSecurity 可以檢測會話何時過期,并采取您指示的特定操作。例如,當用戶使用已過期的會話發出請求時,您可能希望重定向到特定的端點。這是通過 HttpSecurity 中的無效 SessionUrl 實現的:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http.sessionManagement(session -> session.invalidSessionUrl("/invalidSession"));return http.build();
}
請注意,如果使用此機制檢測會話超時,則如果用戶注銷然后在沒有關閉瀏覽器的情況下重新登錄,則可能會錯誤地報告錯誤。這是因為當您使會話無效時,會話 cookie 不會被清除,即使用戶已經登出,它也會被重新提交。如果是這種情況,您可能需要配置注銷以清除會話 cookie。
Customizing the Invalid Session Strategy
ValidSessionUrl 是一種方便的方法,用于使用 SimpleRedirectInvalidSessionStrategy 實現設置 InvalidSessionStrategy。如果希望自定義行為,可以實現 InvalidSessionStrategy 接口,并使用 valididSessionStrategy 方法對其進行配置:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http.sessionManagement(session -> session.invalidSessionStrategy(new MyCustomInvalidSessionStrategy()));return http.build();
}
Clearing Session Cookies on Logout
你可以在注銷時顯式地刪除這個 JSESSIONID cookie,例如在注銷處理程序中使用 Clear-Site-Data 頭:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http.logout((logout) -> logout.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES))));return http.build();
}
這樣做的好處是容器不可知,并且適用于任何支持 Clear-Site-Data 報頭的容器。
作為替代,您還可以在注銷處理程序中使用以下語法:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http.logout(logout -> logout.deleteCookies("JSESSIONID"));return http.build();
}
不幸的是,這并不能保證在每個 servlet 容器中都能正常工作,因此您需要在您的環境中對其進行測試。
Understanding Session Fixation Attack Protection
會話固定攻擊是一種潛在的風險,惡意攻擊者可能通過訪問一個站點來創建一個會話,然后說服另一個用戶使用相同的會話登錄(例如,通過向他們發送一個包含會話標識符作為參數的鏈接)。SpringSecurity 通過創建新會話或在用戶登錄時更改會話 ID 來自動防止這種情況發生。
Configuring Session Fixation Protection
你可以通過選擇三個推薦的選項來控制會話固定保護策略:
- ChangeSessionId-不要創建新的會話,而是使用 Servlet 容器(HttpServletRequest # changeSessionId ())提供的會話固定保護。此選項僅在 Servlet 3.1(JavaEE7)和更新的容器中可用。在舊容器中指定它將導致異常。這是 Servlet 3.1和更新的容器中的默認值。
- NewSession-創建一個新的“ clean”會話,不復制現有的會話數據(仍將復制與 Spring Security 相關的屬性)。
- MigateSession-創建一個新會話并將所有現有會話屬性復制到新會話。這是 Servlet 3.0或更老的容器中的默認值。
您可以通過以下方法配置會話固定保護:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http.sessionManagement((session) -> session.sessionFixation((sessionFixation) -> sessionFixation.newSession()));return http.build();
}
當發生會話固定保護時,會導致在應用程序上下文中發布 SessionFixationProtectionEvent。
如果您使用 changeSessionId,這種保護也會導致任何 jakarta.servlet.http.HttpSessionIdListener 正在通知 ,因此如果您的代碼同時偵聽這兩個事件,請謹慎使用。
您還可以將會話固定保護設置為無,以禁用它,但是不建議這樣做,因為這會使您的應用程序容易受到攻擊。
Using SecurityContextHolderStrategy
考慮以下代碼塊:
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 的機會。為此,應將代碼更改為:
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(); // 1context.setAuthentication(authentication);// 2this.securityContextHolderStrategy.setContext(context);// 3}}
- 使用配置的 SecurityContextHolderStrategy 創建一個空的 SecurityContext 實例。
- 設置 SecurityContext 實例中的 Authentication 對象。
- 設置 SecurityContextHolderStrategy 中的 SecurityContext 實例。
Forcing Eager Session Creation
有時,急切地創建會話可能很有價值。這可以通過使用 ForceEagerSessionCreationFilter 完成,該過濾器可以使用以下方式配置:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS));return http.build();
}
延伸閱讀
使用 Spring Session的集群Seession