SpringSecurity(十五):會話管理


當瀏覽器登錄后,服務器和瀏覽器之間會建立一個會話(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實例。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM