身份驗證永續性和會話管理
一旦你有一個認證請求的應用程式,考慮如何持久化並恢復該認證以用於未來的請求是很重要的。
預設情況下,這是自動完成的,因此不需要額外的程式碼,儘管瞭解HttpSecurity中requireExplicitSave的含義很重要。
如果你願意,你可以閱讀更多關於requireExplicitSave的作用或它為何重要。否則,在大多數情況下,你已經完成了本節內容。
但在離開之前,請考慮以下任何用例是否適合你的應用程式
-
我想要瞭解會話管理的元件
-
我想要限制使用者併發登入的次數
-
我想要自己直接儲存身份驗證,而不是讓Spring Security來做
-
我正在手動儲存身份驗證,我想要移除它
-
我正在使用
SessionManagementFilter,我需要關於如何不再使用它的指導 -
我想要將身份驗證儲存在會話以外的其他地方
-
我正在使用無狀態身份驗證,但我仍然想將其儲存在會話中
-
我正在使用
SessionCreationPolicy.NEVER,但應用程式仍在建立會話。
瞭解會話管理的元件
會話管理支援由一些協同工作的元件組成,以提供功能。這些元件是SecurityContextHolderFilter、SecurityContextPersistenceFilter和SessionManagementFilter。
|
在Spring Security 6中, |
SessionManagementFilter
SessionManagementFilter根據SecurityContextHolder的當前內容檢查SecurityContextRepository的內容,以確定使用者是否在當前請求期間進行了身份驗證,通常是透過非互動式身份驗證機制,例如預身份驗證或記住我[1]。如果倉庫包含安全上下文,過濾器不執行任何操作。如果它不包含,並且執行緒本地SecurityContext包含一個(非匿名)Authentication物件,過濾器假定它們已被堆疊中的前一個過濾器進行身份驗證。然後它將呼叫配置的SessionAuthenticationStrategy。
如果使用者當前未進行身份驗證,過濾器將檢查是否請求了無效的會話ID(例如,由於超時),並且如果設定了InvalidSessionStrategy,則會呼叫它。最常見的行為是重定向到固定URL,這封裝在標準實現SimpleRedirectInvalidSessionStrategy中。後者也用於透過名稱空間配置無效會話URL時,如前所述。
擺脫SessionManagementFilter
在Spring Security 5中,預設配置依賴於SessionManagementFilter來檢測使用者是否剛剛進行了身份驗證並呼叫SessionAuthenticationStrategy。這樣做的問題是,這意味著在典型的設定中,每個請求都必須讀取HttpSession。
在Spring Security 6中,預設是身份驗證機制本身必須呼叫SessionAuthenticationStrategy。這意味著不需要檢測Authentication何時完成,因此每個請求都不需要讀取HttpSession。
擺脫SessionManagementFilter時需要考慮的事項
在Spring Security 6中,預設不使用SessionManagementFilter,因此,sessionManagement DSL中的某些方法將不會產生任何效果。
| 方法 | 替代 |
|---|---|
|
在您的身份驗證機制中配置一個 |
|
在您的身份驗證機制中配置一個 |
|
如上文討論,在您的身份驗證機制中配置一個 |
如果您嘗試使用這些方法中的任何一個,將會丟擲異常。
自定義身份驗證儲存位置
預設情況下,Spring Security 會將安全上下文儲存在 HTTP 會話中。但是,您可能出於以下幾個原因想要自定義它
-
您可能希望在
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,是無狀態的,因此在每個請求上都會重新驗證使用者。
如果您不希望建立會話,可以使用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,並且還阻止了請求被儲存到會話中。
如果您正在使用SessionCreationPolicy.NEVER,您可能會注意到應用程式仍在建立HttpSession。在大多數情況下,這是因為請求被儲存到會話中,以便在身份驗證成功後重新請求已認證的資源。為了避免這種情況,請參閱如何阻止請求被儲存一節。
將會話中的無狀態身份驗證儲存
如果由於某種原因,您正在使用無狀態身份驗證機制,但仍然希望將會話中的身份驗證儲存起來,則可以使用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 Authentication。
理解顯式儲存要求
在 Spring Security 5 中,預設行為是SecurityContext自動儲存到SecurityContextRepository中,透過SecurityContextPersistenceFilter完成。儲存必須在HttpServletResponse提交之前和SecurityContextPersistenceFilter之前完成。不幸的是,SecurityContext的自動持久化可能會讓使用者感到意外,因為它在請求完成之前(即在提交HttpServletResponse之前)完成。跟蹤狀態以確定是否需要儲存也很複雜,有時會導致不必要的寫入SecurityContextRepository(即HttpSession)。
由於這些原因,SecurityContextPersistenceFilter已被棄用,取而代之的是SecurityContextHolderFilter。在Spring Security 6中,預設行為是SecurityContextHolderFilter只會從SecurityContextRepository中讀取SecurityContext並將其填充到SecurityContextHolder中。現在,如果使用者希望SecurityContext在請求之間持久化,則必須使用SecurityContextRepository顯式儲存SecurityContext。這消除了歧義,並透過僅在必要時才寫入SecurityContextRepository(即HttpSession)來提高效能。
工作原理
總而言之,當requireExplicitSave為true時,Spring Security 會設定SecurityContextHolderFilter,而不是SecurityContextPersistenceFilter。
配置併發會話控制
如果您希望對單個使用者登入應用程式的能力施加限制,Spring Security 透過以下簡單的新增即可提供開箱即用的支援。首先,您需要在配置中新增以下監聽器,以使 Spring Security 瞭解會話生命週期事件
-
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>
這將阻止使用者多次登入 - 第二次登入將導致第一次登入失效。
您還可以根據使用者的身份進行調整。例如,管理員可能能夠擁有多個會話
-
Java
-
Kotlin
-
XML
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
AuthorizationManager<?> isAdmin = AuthorityAuthorizationManager.hasRole("ADMIN");
http
.sessionManagement((session) -> session
.maximumSessions((authentication) -> isAdmin.authorize(() -> authentication, null).isGranted() ? -1 : 1)
);
return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val isAdmin: AuthorizationManager<*> = AuthorityAuthorizationManager.hasRole("ADMIN")
http {
sessionManagement {
sessionConcurrency {
maximumSessions {
authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
}
}
}
}
return http.build()
}
<http>
...
<session-management>
<concurrency-control max-sessions-ref="sessionLimit" />
</session-management>
</http>
<b:bean id="sessionLimit" class="my.SessionLimitImplementation"/>
使用 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());
}
}
您可以使用最大會話示例進行嘗試。
通常,您更希望阻止第二次登入,在這種情況下,您可以使用
-
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-authentication-error-url新增到session-management元素中。
使用 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());
}
}
如果您正在使用基於表單登入的自定義身份驗證過濾器,則必須顯式配置併發會話控制支援。您可以使用最大會話阻止登入示例進行嘗試。
|
如果您正在使用 |
檢測超時
會話會自動過期,無需採取任何措施來確保安全上下文被移除。話雖如此,Spring Security 可以檢測會話何時過期並採取您指定的特定操作。例如,當用戶使用已過期的會話發出請求時,您可能希望重定向到特定的端點。這可以透過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>
請注意,如果您使用此機制來檢測會話超時,如果使用者登出後又在不關閉瀏覽器的情況下重新登入,它可能會錯誤地報告錯誤。這是因為會話無效時不會清除會話 cookie,即使使用者已登出,該 cookie 仍會重新提交。如果出現這種情況,您可能需要配置登出以清除會話 cookie。
自定義無效會話策略
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>
登出時清除會話 Cookie
您可以在登出時明確刪除 JSESSIONID cookie,例如透過在登出處理程式中使用Clear-Site-Data header
-
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 容器,因此您需要在自己的環境中進行測試。
|
如果您的應用程式在代理伺服器後面執行,您還可以透過配置代理伺服器來刪除會話 cookie。例如,透過使用 Apache HTTPD 的 |
<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>
理解會話固定攻擊防護
會話固定攻擊是一種潛在的風險,惡意攻擊者可以透過訪問網站建立會話,然後說服另一個使用者使用相同的會話登入(例如,透過向他們傳送包含會話識別符號作為引數的連結)。Spring Security 透過在使用者登入時建立新會話或更改會話 ID 來自動防範此問題。
配置會話固定保護
您可以透過選擇以下三種推薦選項來控制會話固定保護策略
-
changeSessionId- 不建立新會話。相反,使用 Servlet 容器提供的會話固定保護(HttpServletRequest#changeSessionId())。此選項僅在 Servlet 3.1(Java EE 7)及更高版本的容器中可用。在舊版本容器中指定它將導致異常。這是 Servlet 3.1 及更高版本容器中的預設設定。 -
newSession- 建立一個“乾淨”的新會話,不復制現有會話資料(Spring Security 相關屬性仍將被複制)。 -
migrateSession- 建立一個新會話並將所有現有會話屬性複製到新會話。這是 Servlet 3.0 或更早版本容器中的預設設定。
您可以透過以下方式配置會話固定保護
-
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>
當會話固定保護髮生時,它會導致在應用程式上下文中釋出SessionFixationProtectionEvent。如果您使用changeSessionId,此保護**還會**通知任何jakarta.servlet.http.HttpSessionIdListener,因此如果您的程式碼同時監聽這兩個事件,請謹慎使用。
您還可以將會話固定保護設定為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例項。
強制提前建立會話
有時,提前建立會話可能很有價值。這可以透過使用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的叢集會話
SessionManagementFilter檢測到,因為在認證請求期間不會呼叫該過濾器。在這些情況下,會話管理功能必須單獨處理。