集羣化部署,Spring Security 要如何處理 session 共享?

前面和你們聊了 Spring Security 如何像 QQ 同樣,自動踢掉已登陸用戶(Spring Boot + Vue 先後端分離項目,如何踢掉已登陸用戶?),可是前面咱們是基於單體應用的,若是咱們的項目是集羣化部署,這個問題該如何解決呢?java

今天咱們就來看看集羣化部署,Spring Security 要如何處理 session 併發。nginx

本文是 Spring Security 系列第 17 篇,閱讀前面的文章有助於更好的理解本文:git

  1. 挖一個大坑,Spring Security 開搞!
  2. 鬆哥手把手帶你入門 Spring Security,別再問密碼怎麼解密了
  3. 手把手教你定製 Spring Security 中的表單登陸
  4. Spring Security 作先後端分離,咱就別作頁面跳轉了!通通 JSON 交互
  5. Spring Security 中的受權操做原來這麼簡單
  6. Spring Security 如何將用戶數據存入數據庫?
  7. Spring Security+Spring Data Jpa 強強聯手,安全管理只有更簡單!
  8. Spring Boot + Spring Security 實現自動登陸功能
  9. Spring Boot 自動登陸,安全風險要怎麼控制?
  10. 在微服務項目中,Spring Security 比 Shiro 強在哪?
  11. SpringSecurity 自定義認證邏輯的兩種方式(高級玩法)
  12. Spring Security 中如何快速查看登陸用戶 IP 地址等信息?
  13. Spring Security 自動踢掉前一個登陸用戶,一個配置搞定!
  14. Spring Boot + Vue 先後端分離項目,如何踢掉已登陸用戶?
  15. Spring Security 自帶防火牆!你都不知道本身的系統有多安全!
  16. 什麼是會話固定攻擊?Spring Boot 中要如何防護會話固定攻擊?

1.集羣會話方案

在傳統的單服務架構中,通常來講,只有一個服務器,那麼不存在 Session 共享問題,可是在分佈式/集羣項目中,Session 共享則是一個必須面對的問題,先看一個簡單的架構圖:github

在這樣的架構中,會出現一些單服務中不存在的問題,例如客戶端發起一個請求,這個請求到達 Nginx 上以後,被 Nginx 轉發到 Tomcat A 上,而後在 Tomcat A 上往 session 中保存了一份數據,下次又來一個請求,這個請求被轉發到 Tomcat B 上,此時再去 Session 中獲取數據,發現沒有以前的數據。web

1.1 session 共享

對於這一類問題的解決,目前比較主流的方案就是將各個服務之間須要共享的數據,保存到一個公共的地方(主流方案就是 Redis):redis

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aaFMeebv-1589763397129)(http://img.itboyhub.com/2020/...]spring

當全部 Tomcat 須要往 Session 中寫數據時,都往 Redis 中寫,當全部 Tomcat 須要讀數據時,都從 Redis 中讀。這樣,不一樣的服務就可使用相同的 Session 數據了。數據庫

這樣的方案,能夠由開發者手動實現,即手動往 Redis 中存儲數據,手動從 Redis 中讀取數據,至關於使用一些 Redis 客戶端工具來實現這樣的功能,毫無疑問,手動實現工做量仍是蠻大的。後端

一個簡化的方案就是使用 Spring Session 來實現這一功能,Spring Session 就是使用 Spring 中的代理過濾器,將全部的 Session 操做攔截下來,自動的將數據 同步到 Redis 中,或者自動的從 Redis 中讀取數據。瀏覽器

對於開發者來講,全部關於 Session 同步的操做都是透明的,開發者使用 Spring Session,一旦配置完成後,具體的用法就像使用一個普通的 Session 同樣。

1.2 session 拷貝

session 拷貝就是不利用 redis,直接在各個 Tomcat 之間進行 session 數據拷貝,可是這種方式效率有點低,Tomcat A、B、C 中任意一個的 session 發生了變化,都須要拷貝到其餘 Tomcat 上,若是集羣中的服務器數量特別多的話,這種方式不只效率低,還會有很嚴重的延遲。

因此這種方案通常做爲了解便可。

1.3 粘滯會話

所謂的粘滯會話就是將相同 IP 發送來的請求,經過 Nginx 路由到同一個 Tomcat 上去,這樣就不用進行 session 共享與同步了。這是一個辦法,可是在一些極端狀況下,可能會致使負載失衡(由於大部分狀況下,都是不少人用同一個公網 IP)。

因此,Session 共享就成爲了這個問題目前主流的解決方案了。

2.Session共享

2.1 建立工程

首先 建立一個 Spring Boot 工程,引入 Web、Spring Session、Spring Security 以及 Redis:

建立成功以後,pom.xml 文件以下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

2.2 配置

spring.redis.password=123
spring.redis.port=6379
spring.redis.host=127.0.0.1

spring.security.user.name=javaboy
spring.security.user.password=123

server.port=8080

配置一下 Redis 的基本信息;Spring Security 爲了簡化,我就將用戶名密碼直接配置在 application.properties 中了,最後再配置一下項目端口號。

2.3 使用

配置完成後 ,就可使用 Spring Session 了,其實就是使用普通的 HttpSession ,其餘的 Session 同步到 Redis 等操做,框架已經自動幫你完成了:

@RestController
public class HelloController {
    @Value("${server.port}")
    Integer port;
    @GetMapping("/set")
    public String set(HttpSession session) {
        session.setAttribute("user", "javaboy");
        return String.valueOf(port);
    }
    @GetMapping("/get")
    public String get(HttpSession session) {
        return session.getAttribute("user") + ":" + port;
    }
}

考慮到一會 Spring Boot 將以集羣的方式啓動 ,爲了獲取每個請求究竟是哪個 Spring Boot 提供的服務,須要在每次請求時返回當前服務的端口號,所以這裏我注入了 server.port 。

接下來 ,項目打包:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hqUwnFQc-1589763397136)(http://img.itboyhub.com/2020/...]

打包以後,啓動項目的兩個實例:

java -jar session-4-0.0.1-SNAPSHOT.jar --server.port=8080
java -jar session-4-0.0.1-SNAPSHOT.jar --server.port=8081

而後先訪問 localhost:8080/set8080 這個服務的 Session 中保存一個變量,第一次訪問時會自動跳轉到登陸頁面,輸入用戶名密碼進行登陸便可。訪問成功後,數據就已經自動同步到 Redis 中 了 :

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HKNFuzWj-1589763397137)(http://img.itboyhub.com/2020/...]

而後,再調用 localhost:8081/get 接口,就能夠獲取到 8080 服務的 session 中的數據:

此時關於 session 共享的配置就已經所有完成了,session 共享的效果咱們已經看到了。

2.4 Security 配置

Session 共享已經實現了,可是咱們發現新的問題,在Spring Boot + Vue 先後端分離項目,如何踢掉已登陸用戶?一文中咱們配置的 session 併發管理失效了。

也就是說,若是我添加了以下配置:

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest()
            ...
            .sessionManagement()
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true);
}

如今這個配置不起做用,用戶依然能夠在多個瀏覽器上同時登陸。

這是怎麼回事呢?

首先建議你們回憶一下Spring Boot + Vue 先後端分離項目,如何踢掉已登陸用戶?一文。

在該文中,咱們提到,會話註冊表的維護默認是由 SessionRegistryImpl 來維護的,而 SessionRegistryImpl 的維護就是基於內存的維護。如今咱們雖然啓用了 Spring Session+Redis 作 Session 共享,可是 SessionRegistryImpl 依然是基於內存來維護的,因此咱們要修改 SessionRegistryImpl 的實現邏輯。

修改方式也很簡單,實際上 Spring Session 爲咱們提供了對應的實現類 SpringSessionBackedSessionRegistry,具體配置以下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    FindByIndexNameSessionRepository sessionRepository;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest()
                ...
                .sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
                .sessionRegistry(sessionRegistry());
    }
    @Bean
    SpringSessionBackedSessionRegistry sessionRegistry() {
        return new SpringSessionBackedSessionRegistry(sessionRepository);
    }
}

咱們在這裏只須要提供一個 SpringSessionBackedSessionRegistry 的實例,而且將其配置到 sessionManagement 中去便可。之後,session 併發數據的維護將由 SpringSessionBackedSessionRegistry 來完成,而不是 SessionRegistryImpl,如此,咱們關於 session 併發的配置就生效了,在集羣環境下,用戶也只能夠在一臺設備上登陸。

爲了讓咱們的案例看起更完美一些,接下來咱們來引入 Nginx ,實現服務實例自動切換。

3.引入 Nginx

很簡單,進入 Nginx 的安裝目錄的 conf 目錄下(默認是在 /usr/local/nginx/conf),編輯 nginx.conf 文件:

在這段配置中:

  1. upstream 表示配置上游服務器
  2. javaboy.org 表示服務器集羣的名字,這個能夠隨意取名字
  3. upstream 裏邊配置的是一個個的單獨服務
  4. weight 表示服務的權重,意味者將有多少比例的請求從 Nginx 上轉發到該服務上
  5. location 中的 proxy_pass 表示請求轉發的地址,/ 表示攔截到全部的請求,轉發轉發到剛剛配置好的服務集羣中
  6. proxy_redirect 表示設置當發生重定向請求時,nginx 自動修正響應頭數據(默認是 Tomcat 返回重定向,此時重定向的地址是 Tomcat 的地址,咱們須要將之修改使之成爲 Nginx 的地址)。

配置完成後,將本地的 Spring Boot 打包好的 jar 上傳到 Linux ,而後在 Linux 上分別啓動兩個 Spring Boot 實例:

nohup java -jar session-4-0.0.1-SNAPSHOT.jar --server.port=8080 &
nohup java -jar session-4-0.0.1-SNAPSHOT.jar --server.port=8081 &

其中

  • nohup 表示當終端關閉時,Spring Boot 不要中止運行
  • & 表示讓 Spring Boot 在後臺啓動

配置完成後,重啓 Nginx:

/usr/local/nginx/sbin/nginx -s reload

Nginx 啓動成功後,咱們首先手動清除 Redis 上的數據,而後訪問 192.168.66.128/set 表示向 session 中保存數據,這個請求首先會到達 Nginx 上,再由 Nginx 轉發給某一個 Spring Boot 實例:

如上,表示端口爲 8081Spring Boot 處理了這個 /set 請求,再訪問 /get 請求:

能夠看到,/get 請求是被端口爲 8080 的服務所處理的。

4.總結

本文主要向你們介紹了 Spring Session 的使用,另外也涉及到一些 Nginx 的使用 ,雖然本文較長,可是實際上 Spring Session 的配置沒啥,涉及到的配置也都很簡單。

若是你們沒有在 SSM 架構中用過 Spring Session ,可能不太好理解咱們在 Spring Boot 中使用 Spring Session 有多麼方便,由於在 SSM 架構中,Spring Session 的使用要配置三個地方 ,一個是 web.xml 配置代理過濾器,而後在 Spring 容器中配置 Redis,最後再配置 Spring Session,步驟仍是有些繁瑣的,而 Spring Boot 中直接幫咱們省去了這些繁瑣的步驟!

好了 ,本文就說到這裏,本文相關案例我已經上傳到 GitHub ,你們能夠自行下載:https://github.com/lenve/spring-security-samples

若是以爲有收穫,記得點個在看鼓勵下鬆哥哦~

相關文章
相關標籤/搜索