技術(shù)干貨實(shí)戰(zhàn)(4)- 分布式集群部署模式下Nginx如何實(shí)現(xiàn)用戶(hù)登錄Session共享(含詳細(xì)配置與代碼實(shí)戰(zhàn))

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。


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

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


言歸正傳,咱們繼續(xù)聊聊一個(gè)Java開(kāi)發(fā)的系統(tǒng)在分布式集群部署模式下如何實(shí)現(xiàn)用戶(hù)登錄成功后Session的共享功能!在這里debug以之前擼過(guò)的課程“企業(yè)權(quán)限管理平臺(tái)實(shí)戰(zhàn)”中的 系統(tǒng)源碼為例,實(shí)打?qū)嵉亟榻B如何實(shí)現(xiàn)集群部署模式下Session的共享!

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


本文實(shí)操所在的環(huán)境與軟件信息如下:準(zhǔn)備1臺(tái)Linux服務(wù)器 操作系統(tǒng)為Centos7(因?yàn)?span lang="EN-US">debug窮,沒(méi)有買(mǎi)多臺(tái)服務(wù)器,因此下面的集群部署模式主要為單機(jī)多實(shí)例集群部署),Nginx版本為1.16.1Redis版本為6.x,用于演示的系統(tǒng)為:企業(yè)權(quán)限管理管理【簡(jiǎn)化版】(下載地址在文末有提供

OK,我們繼續(xù)往下講!在動(dòng)手實(shí)操之前,我們將擼一擼相關(guān)的理論知識(shí)要點(diǎn)!

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

1Nginx的負(fù)載均衡:所謂的負(fù)載,可以理解為服務(wù)器承擔(dān)的壓力,而在一個(gè)常規(guī)的Web應(yīng)用系統(tǒng)中,壓力主要來(lái)源于前端以及其他應(yīng)用服務(wù)的請(qǐng)求;如果應(yīng)用系統(tǒng)只是部署單例且在一臺(tái)機(jī)器上,那么幾乎就是由這臺(tái)機(jī)器承擔(dān)下了所有的壓力(“終究是一個(gè)人扛下了所有”)

這種方式的弊端顯而易見(jiàn):當(dāng)Web應(yīng)用系統(tǒng)訪問(wèn)量達(dá)到一定的程度后,單機(jī)負(fù)載很可能會(huì)承受不了,萬(wàn)一要是宕了機(jī),應(yīng)用系統(tǒng)也就訪問(wèn)不了了,帶來(lái)的損失難以估量;

因此也就有了“均衡負(fù)載”,專(zhuān)業(yè)術(shù)語(yǔ)叫“負(fù)載均衡”,見(jiàn)名知意:部署多臺(tái)服務(wù)器以便平均分擔(dān)來(lái)自四面八方的壓力,當(dāng)前端發(fā)來(lái)請(qǐng)求時(shí),Nginx會(huì)自動(dòng)根據(jù)某種策略檢查哪臺(tái)服務(wù)器空閑,則將該請(qǐng)求轉(zhuǎn)發(fā)給該服務(wù)器處理;某種程度上講,“負(fù)載均衡”可以用于保證應(yīng)用系統(tǒng)架構(gòu)的高可用。

2)系統(tǒng)部署到Linux后,Nginx配置起到的作用:主要有幾個(gè),一個(gè)是用于充當(dāng)前端http請(qǐng)求處理服務(wù)器(即網(wǎng)頁(yè)等靜態(tài)資源的處理);一個(gè)是請(qǐng)求代理轉(zhuǎn)發(fā)(反向代理),如下所示:   

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地址:應(yīng)用服務(wù)的端口號(hào)/;
}

最后一個(gè)作用則是“負(fù)載均衡”;   


二、動(dòng)手實(shí)操走起:

1)將“權(quán)限管理平臺(tái)【簡(jiǎn)化版】”的源碼下載解壓,并導(dǎo)入IDEA,在本地運(yùn)行沒(méi)問(wèn)題后,點(diǎn)擊右側(cè)的Maven,執(zhí)行clean、install操作,稍等片刻即可打包出一個(gè)Jar包,名為ym-1.0.1.jar;然后在Linux環(huán)境下創(chuàng)建目錄:/srv/dubbo/jiqun,在該目錄下創(chuàng)建3個(gè)文件夾,分別是18081、18082、18083,代表著單臺(tái)機(jī)多個(gè)服務(wù)實(shí)例對(duì)應(yīng)的端口。

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

cd /srv/dubbo/jiqun/18081 進(jìn)入該目錄后執(zhí)行:   

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

cd /srv/dubbo/jiqun/18082 進(jìn)入該目錄后執(zhí)行:   

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

cd /srv/dubbo/jiqun/18083 進(jìn)入該目錄后執(zhí)行:

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

觀察上面多個(gè)服務(wù)實(shí)例打印出來(lái)的日志,如果正常運(yùn)行則進(jìn)入下一步,如果不正常,則按照?qǐng)?bào)錯(cuò)信息自行檢查,然后重新打包部署上傳上去即可(解決問(wèn)題期間有任何問(wèn)題都可以聯(lián)系debug,與debug交流)

3)完了之后,將18081、1808218083 這三個(gè)端口加入到防火墻白名單(iptables或者firewall),同時(shí)也需要在云服務(wù)器提供商ECS的網(wǎng)絡(luò)安全組配置下安全規(guī)則(將端口加入進(jìn)去即可);完了之后就是Nginx的配置了:

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

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

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

如果將 ip_hash前面的注釋去掉,則變?yōu)椋涸吹刂饭?span lang="EN-US">hash,即根據(jù)請(qǐng)求客戶(hù)端(瀏覽器)的IP地址通過(guò)某種hash算法計(jì)算出對(duì)應(yīng)的服務(wù)實(shí)例(比如localhost:18081),那么往后在不做調(diào)整的情況下,該客戶(hù)端幾乎所有的請(qǐng)求將交給 localhost:18081 進(jìn)行處理(可以理解一一綁定);

如果是以下的配置,則該負(fù)載均衡的策略為“加權(quán)輪詢(xún)”,見(jiàn)名知意,它是建立在輪詢(xún)的基礎(chǔ)之上的,只不過(guò)加了個(gè)參數(shù):權(quán)重weight(數(shù)值類(lèi)型),它將表示Nginx在輪番查詢(xún)哪臺(tái)機(jī)/哪個(gè)服務(wù)實(shí)例可用時(shí),weight系數(shù)越大,被命中的幾率將越大!

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

我們暫且選擇默認(rèn)的“輪詢(xún)策略”;完了之后,則是應(yīng)用服務(wù)本身的反向代理(http請(qǐng)求代理服務(wù)配置),如下所示:   

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;
}
}

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

1)在瀏覽器打開(kāi)地址:http://history.huicairj.com/ ,此時(shí)客戶(hù)端會(huì)發(fā)出“獲取驗(yàn)證碼”的請(qǐng)求,會(huì)發(fā)現(xiàn)該請(qǐng)求打在了 18082 那個(gè)服務(wù)實(shí)例上;

2)登錄成功后,會(huì)發(fā)現(xiàn)首頁(yè)也會(huì)發(fā)出“獲取當(dāng)前用戶(hù)登錄信息以及獲取左邊菜單欄”的請(qǐng)求,該請(qǐng)求則打在了 18081 那個(gè)服務(wù)實(shí)例;

3)點(diǎn)擊“用戶(hù)管理”,會(huì)發(fā)現(xiàn)發(fā)出的“查詢(xún)用戶(hù)列表”的請(qǐng)求又打回了 18082服務(wù)實(shí)例上;

4)再點(diǎn)擊“部門(mén)管理”,會(huì)發(fā)現(xiàn)發(fā)出的“查詢(xún)用戶(hù)列表”的請(qǐng)求打在了 18083服務(wù)實(shí)例上………

如下圖所示:


在整個(gè)測(cè)試期間,會(huì)發(fā)現(xiàn)用戶(hù)的Session都是時(shí)刻有效的,可能有些小伙伴會(huì)有疑問(wèn),這是咋做到的呢?我們都知道Session是存儲(chǔ)在服務(wù)端的,更確切的講,它是跟服務(wù)器的某個(gè)應(yīng)用服務(wù)掛鉤的,而現(xiàn)在我們做了集群部署,有3個(gè)服務(wù)實(shí)例,而且根據(jù)上述的演示過(guò)程,我們已經(jīng)得知每次的請(qǐng)求并不是固定的落在某個(gè)服務(wù)實(shí)例上的,也就意味著Session應(yīng)該是存儲(chǔ)在某個(gè)服務(wù)實(shí)例上的吧,如下所示為整個(gè)系統(tǒng)部署的簡(jiǎn)化架構(gòu)模型如下所示:


在上述該架構(gòu)模型中,如果Nginx負(fù)載均衡策略采用的是IP_Hash,即源地址哈希Hash法的話,那么Session將沒(méi)啥問(wèn)題;

但如果是輪詢(xún)策略的話,按照上述單一Shiro的開(kāi)發(fā)模式,那問(wèn)題就很大了,即很可能會(huì)出現(xiàn)“登錄成功后進(jìn)入主頁(yè)時(shí),點(diǎn)擊某個(gè)功能模塊會(huì)彈框提示:用戶(hù)沒(méi)登錄”的現(xiàn)象;歸根結(jié)底還是因?yàn)?span lang="EN-US">SessionId在登錄成功后只存在了某個(gè)服務(wù)實(shí)例上,但是又由于采用的是輪詢(xún)策略,因此很可能后續(xù)的請(qǐng)求會(huì)打在其他服務(wù)實(shí)例上,而其他服務(wù)實(shí)例又沒(méi)有存儲(chǔ)該SessionId,于是乎就認(rèn)為用戶(hù)沒(méi)登錄!

因此,如果Nginx配置的負(fù)載均衡策略是“輪詢(xún)”,那么需要在項(xiàng)目上Shiro層面的開(kāi)發(fā)做下改進(jìn),思路為“構(gòu)建一虛擬的Session共享服務(wù)器”,于是乎我們就搬上了Redis,其調(diào)整后的整個(gè)系統(tǒng)部署的簡(jiǎn)化架構(gòu)模型如下所示:


如下所示為調(diào)整后的Shiro + Redis的配置:   

/**
* 顯示自定義注入配置shiro+redis的相關(guān)組件
* @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());

//自定義一個(gè)存儲(chǔ)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));
//鏈接超時(shí)
redisManager.setTimeout(env.getProperty("spring.redis.timeout",Integer.class));
//緩存key時(shí)效
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層的實(shí)現(xiàn),通過(guò)redis - 使用的是shiro-redis開(kāi)源插件
@Bean
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO=new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());

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

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

//設(shè)定用戶(hù)沒(méi)有登錄認(rèn)證時(shí)的跳轉(zhuǎn)鏈接、沒(méi)有授權(quán)時(shí)的跳轉(zhuǎn)鏈接
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setUnauthorizedUrl("/");

//過(guò)濾器鏈配置
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配置相關(guān)的依賴(lài)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>

從該配置中可以得知,其實(shí)就是將后端服務(wù)實(shí)例產(chǎn)生的SessionID存儲(chǔ)到具有獨(dú)立的、中間角色性質(zhì)的緩存中,即緩存中間件Redis里,而不依賴(lài)于任何一臺(tái)服務(wù)器、任何一個(gè)服務(wù)實(shí)例;




OK,打完收工!咱們下期再見(jiàn)?。?!

總結(jié)

1代碼下載:關(guān)注微信公眾號(hào): 程序員實(shí)戰(zhàn)基地 (掃描下圖微信公眾號(hào)即可),回復(fù)數(shù)字: 101 ,即可獲取本文實(shí)操演示用的源碼數(shù)據(jù)庫(kù),即“企業(yè)權(quán)限管理平臺(tái)【簡(jiǎn)化版】”

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


 我是debug,一個(gè)相信技術(shù)改變生活、技術(shù)成就夢(mèng)想 的攻城獅;如果本文對(duì)你有幫助,請(qǐng)關(guān)注公眾號(hào),并動(dòng)動(dòng)手指收藏、點(diǎn)贊、以及轉(zhuǎn)發(fā)哦?。。?/span>