技術干貨實戰(4)- 分布式集群部署模式下Nginx如何實現用戶登錄Session共享(含詳細配置與代碼實戰)

作者: 修羅debug
版權聲明:本文為博主原創文章,遵循 CC 4.0 by-sa 版權協議,轉載請附上原文出處鏈接和本聲明。


最近有小伙伴催更,讓debug多寫點技術干貨,以便多學習、鞏固一些技能;沒辦法,debug也只好應承下來,再忙也要擠出時間擼一擼,以對得起時常關注debug的那些看官老爺們! 本文將重點介紹:Nginx如何進行配置從而實現用戶登錄成功后Session共享的功能,其中我們將以“企業權限管理平臺”為例,加入Redis最終真正實現Session共享的效果(真正的代碼落地哈?。?span style="" lang="EN-US">

說在前面的話:debug近幾個是真的忙,各種項目、產品開發進度跟進、上線,都快累成了狗,但累歸累,生活還是要繼續,夢想還是要追尋!于是乎debug用了差不多3個月的時間,早起晚睡又肝了一本新書:《Spring Boot企業級項目-入門到精通》,雙12期間應該可以出版!為了能讓大家一睹為快,先貼一下封面吧,后續debug會專門出篇文章專門介紹這本書(同時提供優惠購書渠道)!   


言歸正傳,咱們繼續聊聊一個Java開發的系統在分布式集群部署模式下如何實現用戶登錄成功后Session的共享功能!在這里debug以之前擼過的課程“企業權限管理平臺實戰”中的 系統源碼為例,實打實地介紹如何實現集群部署模式下Session的共享!

其中,該課程的觀看地址:https://www.fightjava.com/web/index/course/detail/8 ,也可以找debug咨詢學習該課程;但為了降低各位小伙伴學習的門檻,debug特意將該系統抽絲剖繭,得出個可運行的簡化版,其源碼數據庫等資料的下載地址在文末有提供,各位看官老爺們只需要下載解壓即可照著本文介紹的步驟往下擼?。ㄇ屑裳鄹呤值凸?,不僅僅要看懂,更希望諸位能擼懂,真正地去動一動手?。?/span>


本文實操所在的環境與軟件信息如下:準備1Linux服務器 操作系統為Centos7(因為debug窮,沒有買多臺服務器,因此下面的集群部署模式主要為單機多實例集群部署),Nginx版本為1.16.1,Redis版本為6.x,用于演示的系統為:企業權限管理管理【簡化版】(下載地址在文末有提供

OK,我們繼續往下講!在動手實操之前,我們將擼一擼相關的理論知識要點!

一、理論的東西(不多,很快哈?。?/span>

1Nginx的負載均衡:所謂的負載,可以理解為服務器承擔的壓力,而在一個常規的Web應用系統中,壓力主要來源于前端以及其他應用服務的請求;如果應用系統只是部署單例且在一臺機器上,那么幾乎就是由這臺機器承擔下了所有的壓力(“終究是一個人扛下了所有”)

這種方式的弊端顯而易見:當Web應用系統訪問量達到一定的程度后,單機負載很可能會承受不了,萬一要是宕了機,應用系統也就訪問不了了,帶來的損失難以估量;

因此也就有了“均衡負載”,專業術語叫“負載均衡”,見名知意:部署多臺服務器以便平均分擔來自四面八方的壓力,當前端發來請求時,Nginx會自動根據某種策略檢查哪臺服務器空閑,則將該請求轉發給該服務器處理;某種程度上講,“負載均衡”可以用于保證應用系統架構的高可用。

2)系統部署到Linux后,Nginx配置起到的作用:主要有幾個,一個是用于充當前端http請求處理服務器(即網頁等靜態資源的處理);一個是請求代理轉發(反向代理),如下所示:   

location / {
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header remote_addr $remote_addr;
proxy_pass http://ip地址:應用服務的端口號/;
}

最后一個作用則是“負載均衡”;   


二、動手實操走起:

1)將“權限管理平臺【簡化版】”的源碼下載解壓,并導入IDEA,在本地運行沒問題后,點擊右側的Maven,執行clean、install操作,稍等片刻即可打包出一個Jar包,名為ym-1.0.1.jar;然后在Linux環境下創建目錄:/srv/dubbo/jiqun,在該目錄下創建3個文件夾,分別是18081、18082、18083,代表著單臺機多個服務實例對應的端口。

2)將打包出來的ym-1.0.1.jar通過winscp等工具上傳至/srv/dubbo/jiqun/18081、/srv/dubbo/jiqun/18082、/srv/dubbo/jiqun/18083  3個目錄,緊接著,cd切換到上面3個目錄,然后執行以下的命令:

cd /srv/dubbo/jiqun/18081 進入該目錄后執行:   

nohup java -jar ym-1.0.1.jar --server.port=18081 &
tail -f nohup.out

cd /srv/dubbo/jiqun/18082 進入該目錄后執行:   

nohup java -jar ym-1.0.1.jar --server.port=18082 &
tail -f nohup.out

cd /srv/dubbo/jiqun/18083 進入該目錄后執行:

nohup java -jar ym-1.0.1.jar --server.port=18083 &
tail -f nohup.out

觀察上面多個服務實例打印出來的日志,如果正常運行則進入下一步,如果不正常,則按照報錯信息自行檢查,然后重新打包部署上傳上去即可(解決問題期間有任何問題都可以聯系debug,與debug交流)

3)完了之后,將18081、18082、18083 這三個端口加入到防火墻白名單(iptables或者firewall),同時也需要在云服務器提供商ECS的網絡安全組配置下安全規則(將端口加入進去即可);完了之后就是Nginx的配置了:

首先是配置個服務器組(單機多實例,即多端口的組名;如果是多機實例,則只需要將這行配置加入到每臺機的Nginx配置文件nginx.conf即可)   :

upstream  debug-server {
#ip_hash;
server localhost:18081;
server localhost:18082;
server localhost:18083;
}

其中,ip_hash被我們注釋起來了,則此時的集群負載均衡策略為默認的“輪詢”,所謂的“輪詢”,顧名思義就是輪番檢查哪臺機/哪個服務實例目前處于空閑狀態,如果有資源(CPU/內存)閑置,則交給那臺機/那個服務器實例處理即可!

如果將 ip_hash前面的注釋去掉,則變為:源地址哈希hash,即根據請求客戶端(瀏覽器)的IP地址通過某種hash算法計算出對應的服務實例(比如localhost:18081),那么往后在不做調整的情況下,該客戶端幾乎所有的請求將交給 localhost:18081 進行處理(可以理解一一綁定);

如果是以下的配置,則該負載均衡的策略為“加權輪詢”,見名知意,它是建立在輪詢的基礎之上的,只不過加了個參數:權重weight(數值類型),它將表示Nginx在輪番查詢哪臺機/哪個服務實例可用時,weight系數越大,被命中的幾率將越大!

upstream  debug-server {
server localhost:18081 weight=2;
server localhost:18082 weight=8;
server localhost:18083 weight=6;
}

我們暫且選擇默認的“輪詢策略”;完了之后,則是應用服務本身的反向代理(http請求代理服務配置),如下所示:   

server{
listen 80;
server_name history.huicairj.com;

charset utf8;
location / {
proxy_pass http://debug-server;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

在上述配置中,我們配置了個域名:history.huicairj.com (當然,如果沒有域名的話也沒關系,可以用本機公網IP代替即可), 配置好之后,進入到Nginx的安裝目錄,重啟Nginx即可:/sbin/nginx -s reload

1)在瀏覽器打開地址:http://history.huicairj.com/ ,此時客戶端會發出“獲取驗證碼”的請求,會發現該請求打在了 18082 那個服務實例上;

2)登錄成功后,會發現首頁也會發出“獲取當前用戶登錄信息以及獲取左邊菜單欄”的請求,該請求則打在了 18081 那個服務實例;

3)點擊“用戶管理”,會發現發出的“查詢用戶列表”的請求又打回了 18082服務實例上;

4)再點擊“部門管理”,會發現發出的“查詢用戶列表”的請求打在了 18083服務實例上………

如下圖所示:


在整個測試期間,會發現用戶的Session都是時刻有效的,可能有些小伙伴會有疑問,這是咋做到的呢?我們都知道Session是存儲在服務端的,更確切的講,它是跟服務器的某個應用服務掛鉤的,而現在我們做了集群部署,有3個服務實例,而且根據上述的演示過程,我們已經得知每次的請求并不是固定的落在某個服務實例上的,也就意味著Session應該是存儲在某個服務實例上的吧,如下所示為整個系統部署的簡化架構模型如下所示:


在上述該架構模型中,如果Nginx負載均衡策略采用的是IP_Hash,即源地址哈希Hash法的話,那么Session將沒啥問題;

但如果是輪詢策略的話,按照上述單一Shiro的開發模式,那問題就很大了,即很可能會出現“登錄成功后進入主頁時,點擊某個功能模塊會彈框提示:用戶沒登錄”的現象;歸根結底還是因為SessionId在登錄成功后只存在了某個服務實例上,但是又由于采用的是輪詢策略,因此很可能后續的請求會打在其他服務實例上,而其他服務實例又沒有存儲該SessionId,于是乎就認為用戶沒登錄!

因此,如果Nginx配置的負載均衡策略是“輪詢”,那么需要在項目上Shiro層面的開發做下改進,思路為“構建一虛擬的Session共享服務器”,于是乎我們就搬上了Redis,其調整后的整個系統部署的簡化架構模型如下所示:


如下所示為調整后的Shiro + Redis的配置:   

/**
* 顯示自定義注入配置shiro+redis的相關組件
* @Author:debug (SteadyJack)
* @Date: 2019/9/11 18:01
**/
@Configuration
public class ShiroRedisConfig implements EnvironmentAware {
private Environment env;

@Override
public void setEnvironment(Environment environment) {
this.env=environment;
}

//securityManager-管理subject
@Bean
public SecurityManager securityManager(UserRealm userRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
securityManager.setRememberMeManager(null);

//自定義緩存管理器-redis
securityManager.setCacheManager(cacheManager());

//自定義一個存儲session的管理器
securityManager.setSessionManager(sessionManager());
return securityManager;
}

//自定義session緩存管理器
@Bean
public RedisCacheManager cacheManager(){
RedisCacheManager cacheManager=new RedisCacheManager();
cacheManager.setRedisManager(redisManager());
return cacheManager;
}

@Bean
public RedisManager redisManager(){
RedisManager redisManager=new RedisManager();
redisManager.setHost(env.getProperty("spring.redis.host"));
redisManager.setPort(env.getProperty("spring.redis.port",Integer.class));
//鏈接超時
redisManager.setTimeout(env.getProperty("spring.redis.timeout",Integer.class));
//緩存key時效
redisManager.setExpire(env.getProperty("spring.redis.expire",Integer.class));
return redisManager;
}

//自定義session管理器
public DefaultWebSessionManager sessionManager(){
DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}

//shiro sessionDao層的實現,通過redis - 使用的是shiro-redis開源插件
@Bean
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO=new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());

//設置存儲在緩存中session的Key的前綴
redisSessionDAO.setKeyPrefix("shiro_redis_session:");
return redisSessionDAO;
}

//過濾鏈配置
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);

//設定用戶沒有登錄認證時的跳轉鏈接、沒有授權時的跳轉鏈接
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setUnauthorizedUrl("/");

//過濾器鏈配置
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/swagger/**", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/webjars/**", "anon");
filterMap.put("/swagger-resources/**", "anon");

filterMap.put("/statics/**", "anon");
filterMap.put("/fonts/**", "anon");
filterMap.put("/image/**", "anon");
filterMap.put("/login.html", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/favicon.ico", "anon");
filterMap.put("/captcha.jpg", "anon");

filterMap.put("/**","authc");

shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}

//shiro bean生命周期的管理
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}

其中,要記得在pom.xmlserver模塊)中加入shiro redis配置相關的依賴Jar:   

<!-- shiro+redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>

從該配置中可以得知,其實就是將后端服務實例產生的SessionID存儲到具有獨立的、中間角色性質的緩存中,即緩存中間件Redis里,而不依賴于任何一臺服務器、任何一個服務實例;




OK,打完收工!咱們下期再見?。?!

總結

1代碼下載:關注微信公眾號: 程序員實戰基地 (掃描下圖微信公眾號即可),回復數字: 101 ,即可獲取本文實操演示用的源碼數據庫,即“企業權限管理平臺【簡化版】”

2)本文的內容來源于debug最新擼的課程:Java工程師核心技術-典型案例與面試實戰系列二(基于Spring Boot2.0  感興趣的小伙伴可以前往 fightjava.com 的課程中心進行學習,地址為:https://www.fightjava.com/web/index/course/detail/16


 我是debug,一個相信技術改變生活、技術成就夢想 的攻城獅;如果本文對你有幫助,請關注公眾號,并動動手指收藏、點贊、以及轉發哦?。?!