當瀏覽器登錄后,服務器和瀏覽器之間會建立一個會話(Session), 瀏覽器在每次發送請求時都會攜帶一個SessionId,服務端根據這個SessionId判斷用戶身份。瀏覽器關閉后Session不會自動銷毀。需要開發者手動在服務端調用Session銷毀方法,或者等待Session過期自動銷毀。
在Spring Security中與HttpSession有關的功能由SessionManagementFilter和SessionAuthenticationStrategy接口處理,SessionManagementFilter將Session相關操作委托給SessionAuthenticationStrategy接口去完成。
會話並發管理
會話並發管理是指在當前系統中,同一個用戶可以同時創建多少個會話,如果一台設備對應一個會話,也可以理解為同一個用戶可以同時在多少個設備上進行登錄。
在Spring Security中默認情況下,同一個用戶在多少個設備上登錄並沒有限制,但是我們可以自己設置。
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement()
.maximumSessions(1);
}
}
(1)在configure(HttpSecurity)方法中通過sessionManagerment()方法開啟會話配置,並設置並發數為1
(2)提供一個httpSessionEventPublisher實例。Spring Security中通過一個Map集合來維護當前HttpSession記錄,進而實現會話的並發管理,當用戶登錄成功后,就向集合添加一條HttpSession記錄,會話銷毀后,就從集合移除一條HttpSession記錄。HttpSessionEventPublisher實現了HttpSessionListener接口,可以監聽到HttpSession的創建和銷毀事件,並將HttpSession的創建和銷毀事件發布出去,這樣當有HttpSession銷毀時,Spring Security就可以感知到該事件了
我們也可以自定義銷毀后的行為
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement()
.maximumSessions(1)
.expiredUrl("/expireSession");
}
}
表明被擠下線后訪問/expireSession這個請求,我們可以在controller中為這個請求返回頁面或json數據
當然這一種“擠下線”的行為,我們也可以定義后者無法登陸
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/expireSession");
}
}
會話固定攻擊與防御
會話固定攻擊是一種潛在的風險,惡意攻擊者可能通過訪問當前應用程序來創建會話,然后誘導用戶以相同的會話id登陸(通常是將會話ID作為請求參數放在請求連接中然后誘導用戶去點擊) 進而獲取用戶的登錄身份
舉一個簡單的例子:
(1)攻擊者自己可以正常訪問javaboy網站,在訪問的過程中,網站給攻擊者分配了一個sessionId
(2)攻擊者利用自己的SessionId構造了一個javaboy的鏈接,並把鏈接發給受害者
(3)受害者訪問該鏈接,登錄網站,一個合法的會話就建立了
(4)攻擊者利用手里的sessionId冒充受害者
如果網站支持URL重寫就更容易了!(支持把sessionId放在請求地址中)
Spring Security從三個方面防范會話固定攻擊:
(1)Spring Security自帶Http防火牆,如果sessionid放在地址欄中,這個請求就會直接被攔截下來
(2)在Http響應的Set-Cookie字段中有httpOnly屬性,這樣避免了通過XSS攻擊來獲取Cookie中的會話信息,進而達成會話固定攻擊
(3)既然會話固定攻擊是由於sessionid不變導致的,那么其中一個解決方案就是在用戶登錄成功后,改變sessionid,Spring Security中默認實現了這種方案,實現類就是ChangeSessionIdAuthenticationStrategy
前兩種都是默認行為,一般來說不需要更改。第三種方案在Spring Security中有幾種不同的配置策略,我們先來看以下配置方式:
http.sessionManagement().sessionFixation().changeSessionId();
通過sessionFixation()方法開啟會話固定攻擊防御的配置,一共有四種不同的策略,不同策略對應了不同的SessionAuthenticationStrategy:
(1)changeSessionId():用戶登錄成功后,直接修改HttpSession的SessionId即可,默認方案即此,對應的處理類是ChangeSessionIdAuthenticationStrategy
(2)none():用戶登錄成功后,HttpSession不做任何變化,對應的處理類是NullAuthenticatedSessionStrategy
(3)migrateSession():用戶登錄成功后,創建一個新的HttpSession對象,並將舊的HttpSession中的數據拷貝到新的HttpSession中,對應的處理類是SessionFixationProtectionStrategy
(4)newSession():用戶登錄成功后,穿件一個新的HttpSession對象,對應的處理類也是SessionFixationProtectionStrategy,只不過將其里邊的migrateSessionAttributes屬性設置為false。需要注意的是,該方法並非所有的屬性都不可拷貝,一些Spring Security使用的屬性,如請求緩存,還是會從舊的HttpSession上復制到新的HttpSession。
Session共享
需要注意的是,我們這里討論的范疇是有狀態登錄,如果用戶采用無狀態的認證方式,那么就不涉及會話。也不存在我們接下來要討論的問題。
為了解決集群環境下的會話問題,我們有三種方案:
(1)Session復制:多個服務之間互相復制Session信息,這樣每個服務中都包含有所有的Session信息了,Tomcat通過IP組播對這種方案提供支持。但是這種方案占用帶寬,有時延,服務數量越多效率越低,所以使用較少
(2)Session粘滯:也叫會話保持,就是在Nginx上通過一致性Hash,將Hash結果相同的請求總是分發到一個服務上去,這種方案可以解決一部分集群會話帶來的問題,但是無法解決集群會話並發管理問題
(3)Session共享:Session共享就是將不同服務的會話統一放在一個地方,所有的服務共享一個會話,一般使用一些Key-Value數據庫來存儲Session,例如在redis中,比較常見的方案是使用redis存儲,session共享方案由於其簡便性和穩定性,是目前使用較多的方案。
Session共享目前使用較多的是spring-session,利用spring-session可以方便的實現session的管理
1.引入redis,spring security,spring session的依賴
2.配置redis連接信息
3.修改配置類
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
FindByIndexNameSessionRepository sessionRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf()
.disable()
.sessionManagement()
.maximumSessions(1)
.sessionRegistry(sessionRegistry());
}
@Bean
SpringSessionBackedSessionRegistry sessionRegistry(){
return new SpringSessionBackedSessionRegistry(sessionRepository);
}
}
在這段配置中,我們首先注入一個FindByIndexNameSessionRepository對象,這是一個會話的存儲和加載工具。在前面的案例中,會話信息是保存在內存中,現在的會話信息保存在redis中,具體的保存和加載工程則由FindByIndexNameSessionRepository接口的實現類來完成,默認是RedisIndexedSessionRepository即我們一開始注入的實際是一個RedisIndexedSessionRepository類型的對象
接下來我們還配置了一個SpringSessionBackedSessionRegistry實例,構建時傳入sessionRepository,SpringSessionBackedSessionRegistry繼承自SessionRegistry,用來維護會話信息注冊表
最后在HttpSecurity中配置sessionRegistry即可,相當於spring-session提供的SpringSessionBackedSessionRegistry接管了會話信息注冊表的維護工作。
需要注意的是引入spring-session后不需要在配置HttpSessionEventPublicsher實例,引入spring-session通過SessionRepositoryFilter將請求對象重新封裝為SessionRepositoryRequestWrapper,並重寫了getSession方法,在重寫的getSession方法中,最終返回的是HttpSessionWrapper實例,而在HttpSessionWrapper定義時,就重寫了invalidate方法,當調用會話的invalidate方法去銷毀會話時,就會調用RedisIndexedSessionRepository中的方法,從Redis中移除相應的會話信息,所以不再需要HttpSessionEventPublisher實例。
