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

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


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

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


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

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


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

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

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

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

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

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

2)系統(tǒng)部署到Linux后,Nginx配置起到的作用:主要有幾個,一個是用于充當(dāng)前端http請求處理服務(wù)器(即網(wǎng)頁等靜態(tài)資源的處理);一個是請求代理轉(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ù)的端口號/;
}

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


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

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

2)將打包出來的ym-1.0.1.jar通過winscp等工具上傳至/srv/dubbo/jiqun/18081/srv/dubbo/jiqun/18082、/srv/dubbo/jiqun/18083  3個目錄,緊接著,cd切換到上面3個目錄,然后執(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

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

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

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

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

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

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

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

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

我們暫且選擇默認(rèn)的“輪詢策略”;完了之后,則是應(yīng)用服務(wù)本身的反向代理(http請求代理服務(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;
}
}

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

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

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

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

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

如下圖所示:


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


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

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

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

//自定義一個存儲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層的實(shí)現(xiàn),通過redis - 使用的是shiro-redis開源插件
@Bean
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO=new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());

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

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

//設(shè)定用戶沒有登錄認(rèn)證時的跳轉(zhuǎn)鏈接、沒有授權(quán)時的跳轉(zhuǎn)鏈接
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配置相關(guān)的依賴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存儲到具有獨(dú)立的、中間角色性質(zhì)的緩存中,即緩存中間件Redis里,而不依賴于任何一臺服務(wù)器、任何一個服務(wù)實(shí)例;




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

總結(jié)

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

2)本文的內(nè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,一個相信技術(shù)改變生活、技術(shù)成就夢想 的攻城獅;如果本文對你有幫助,請關(guān)注公眾號,并動動手指收藏、點(diǎn)贊、以及轉(zhuǎn)發(fā)哦?。。?/span>