分析為什么采用集群策略 集群Session共享問題 實現(xiàn)SSO

原生HttpSession解決集群Session共享問題 實現(xiàn)SSO單點登錄

在介紹本節(jié)內(nèi)容之前,在這里談?wù)勎医佑|到的一些后端架構(gòu)出現(xiàn)的問題

就在前兩天輔導(dǎo)員早上9點突然發(fā)布一條選課通知,到中午12點之前完成大三下學(xué)期的選課,好的,我打開了鏈接想著4個小時的選課時間怎么選不上?然而還真沒選上

問題出現(xiàn)

  • 請求超時

    仔細(xì)看了一下之后大概得出了結(jié)論,這個web選課應(yīng)用后端使用php編寫,部署到了Apache服務(wù)器上,查閱了一下php部署在Apache的集群方式更多人叫它拓展用用服務(wù)器組,個人感覺沒有配置應(yīng)用服務(wù)器組,不然全院四個年級加起來也不夠5000的流量怎么會做不到

    我查閱了一下,因為自己沒有使用過Apache服務(wù)器,大概談一下我對這個問題的認(rèn)識,Apache服務(wù)器有自己的幾種工作模式,并且給我感覺有一套自己的進(jìn)程管理體系,類似于線程池,為了減少建立進(jìn)程去處理請求的額外開銷,啟動Apache服務(wù)器的時候,就會建立默認(rèn)配置的空閑進(jìn)程等待請求的到來去處理,(Apache是以進(jìn)程為基礎(chǔ)的結(jié)構(gòu),進(jìn)程要比線程消耗更多的系統(tǒng)開支,不太適合于多處理器環(huán)境,因此,在一個Apache Web站點擴(kuò)容時,通常是增加服務(wù)器或擴(kuò)充群集節(jié)點而不是增加處理器),而在啟動Tomcat的時候能夠發(fā)現(xiàn)進(jìn)程其實只有Tomcat進(jìn)程,但是它其中的線程卻存在許多。這是兩者不太一樣的地方

  • Apache服務(wù)器與Tomcat服務(wù)器的區(qū)別

    • Apache多被稱為web服務(wù)器,并且對Linux支持的相當(dāng)完美,Apache是以進(jìn)程為基礎(chǔ)的結(jié)構(gòu),進(jìn)程要比線程消耗更多的系統(tǒng)開支,不太適合于多處理器環(huán)境,因此,在一個Apache Web站點擴(kuò)容時,通常是增加服務(wù)器或擴(kuò)充群集節(jié)點而不是增加處理器
    • Apache給我的感覺和Nginx效果和功能一樣,都是web服務(wù)器,但是Apache支持php拓展模塊,也就使得php的后端應(yīng)用程序能夠使用它作為載體,也就和Tomcat這種基于J2EE規(guī)范的應(yīng)用服務(wù)器可以和Nginx配置集群使Nginx作為負(fù)載均衡服務(wù)器來使用
  • 舉個帖子中的例子

    • Apache是一輛卡車,上面可以裝一些東西如html等。但是不能裝水,要裝水必須要有容器(桶),Tomcat就是一個桶(裝像Java這樣的水),而這個桶也可以不放在卡車上。
    • Apache只支持靜態(tài)網(wǎng)頁,但像jsp等動態(tài)網(wǎng)頁就需要Tomcat這種應(yīng)用服務(wù)器來處理。
    • Apache和Tomcat整合使用:如果客戶端請求的是靜態(tài)頁面,則只需要Apache服務(wù)器響應(yīng)請求;如果客戶端請求動態(tài)頁面,則是Tomcat這種應(yīng)用服務(wù)器服務(wù)器響應(yīng)請求;
    • 因為jsp是服務(wù)器端解釋代碼的,這樣整合就可以減少Tomcat的服務(wù)開銷 。

終于請求到了登錄頁卻執(zhí)行不了登錄操作

  • 驗證碼錯誤

    經(jīng)過無數(shù)次的刷新嘗試之后總算有一條剛剛忙碌完的進(jìn)程顧及到了我,這個時候,我開始執(zhí)行了登錄操作,卻提示我驗證碼失敗,我校驗了很多次卻不能夠成功登錄,這個時候我又分析了一下,因為自己也實現(xiàn)過驗證碼登錄的邏輯,所以說這個流程還是掌握的比較清楚的

    請求登錄頁的時候,請求后端獲取驗證碼的接口,這個時候后端如果不使用Redis緩存的技術(shù)去解決驗證碼的校驗,最簡單的方式就是放置在session中,key可為一個常亮,我們就叫LOGIN_CODE_SESSION_KEY那么值的話很好理解就是驗證碼的值了,再次請求登錄接口的時候,可以實現(xiàn)一個過濾器去過濾登錄借口,校驗請求中的驗證碼是否與session中的驗證碼值匹配

    那么為什么會提示驗證碼錯誤導(dǎo)致驗證碼錯誤進(jìn)一步致使登錄失敗呢

    • 可以想想這樣一種情況,Apache服務(wù)器的進(jìn)程數(shù)已經(jīng)到達(dá)了接近極限的地步,這種情況下?lián)Q做是什么服務(wù)器我想效率的話肯定低得不能再低甚至可能發(fā)生宕機(jī)問題,我在登錄的時候有點擊過驗證碼的動作,但是卻得不到任何響應(yīng),可以再這樣想一下,因為后端服務(wù)器的負(fù)擔(dān)太重,生成驗證碼的邏輯已經(jīng)執(zhí)行,但是在頁面上因為效率太慢,響應(yīng)沒有及時到達(dá),web頁面沒有刷新最新的驗證碼,導(dǎo)致我們驗證時攜帶過期驗證碼進(jìn)行登錄,提示登錄失敗驗證碼錯誤
  • 登錄壓根沒響應(yīng)

    下面會介紹怎么成了一個沒有響應(yīng)的web應(yīng)用

服務(wù)器未響應(yīng)

  • 服務(wù)器宕機(jī)

    服務(wù)器沒有響應(yīng)這個東西我曾經(jīng)折騰實驗室服務(wù)器的時候就出現(xiàn)過這種尷尬的情況,那會兒造成的錯誤還不是一臺軟件級別的服務(wù)器宕機(jī),而是整個一臺物理級別的服務(wù)器宕機(jī)...難怪怎么用ssh想要上去都沒用,很快很多線上應(yīng)用就開始找我了,然而我還很懵逼,和畢業(yè)的學(xué)長分析了一下,沒錯是關(guān)機(jī)了..

  • 服務(wù)器為什么會宕機(jī)

    簡單說一下服務(wù)器這個概念,在物理級別的服務(wù)器這個概念,簡單一點來說,它是一臺機(jī)器,機(jī)房里面很多個大機(jī)箱基本就是這個了,軟件級別的服務(wù)器是什么,類似Nginx Apache Tomcat 這類的web服務(wù)器和應(yīng)用服務(wù)器

    至于web服務(wù)器和應(yīng)用服務(wù)器我就不在這里贅述,下面來分析一下服務(wù)器為什么會宕機(jī)

    先說說我搞崩的實驗室云服務(wù)器,上面部署了很多應(yīng)用服務(wù)器node的tomcat好像還有php的之類應(yīng)用服務(wù)器,上面的應(yīng)用也就更不用說,實驗室官網(wǎng)可以去參觀一下 www.xiyoumobile.com 學(xué)長學(xué)姐們的心血,真的很贊,尤其是在我搞崩之后覺得有點對不起他們,但是學(xué)長還是給我鼓勵,說正題,我造成的線上事故是因為暑假寫的SpringBoot項目需要部署,并且因為一些接口只能通過學(xué)校的內(nèi)網(wǎng)才能夠訪問爬到數(shù)據(jù),這個時候果斷想到了折騰一下實驗室服務(wù)器,但是沒有經(jīng)驗的我按照原始方式簡單的打了.war包移除內(nèi)置Tomcat之后放在上面,當(dāng)時還沒事,直到第二天早上我知道的時候應(yīng)該已經(jīng)關(guān)機(jī)了幾個小時了

    原因

    SpringBoot在我看來是Spring官方為了簡化基于Spring框架組件的一套為了簡化自身開發(fā)的框架,說句實話用起來很方便,但是也正是因為他的方便,其中很多依賴關(guān)系以及Bean的依賴,組裝變得規(guī)模很龐大,使用一些提供的支持的時候也只是去操作高度封裝的Api接口,看過一些源碼,確實覺得寫的很好,這個時候會造成什么問題呢,Jvm方法區(qū)正是因為有了這么多的Bean以及一些動態(tài)代理類的信息,硬生生地讓整個SpringBoot后端服務(wù)占到了可能高于2G的內(nèi)存,實驗室服務(wù)器因為申請的早,后來才知道是動態(tài)4G內(nèi)存,再加上之前上面那么多東西,后來想想自己真的是有點弱智...也是因為當(dāng)時對Jvm沒有什么了解,以至于沒有意識到Jvm的簡單調(diào)優(yōu),導(dǎo)致實驗室服務(wù)器內(nèi)存耗盡宕機(jī)最終關(guān)機(jī)

    服務(wù)器宕機(jī)的原因

    就像我上文一樣,物理級別的服務(wù)器宕機(jī)的原因,要么是創(chuàng)建的進(jìn)程過多,占用內(nèi)存過多,導(dǎo)致操作系統(tǒng)調(diào)度變慢,以至于到最后不能合理地去管理進(jìn)程回收一些空閑進(jìn)程,導(dǎo)致內(nèi)存一直持續(xù)過高占用,這個時候如果有新的進(jìn)程需要執(zhí)行任務(wù),可能就會出現(xiàn)死機(jī)的情況,進(jìn)而就關(guān)機(jī)了,像這種情況,可以分析Jvm的GC情況,可能是自己的編碼導(dǎo)致一直存在某些引用持有一些本該被GC的引用,導(dǎo)致GC的時候并沒有將其回收導(dǎo)致的問題,可能最后還會出現(xiàn)OOM的問題,分析起來還是挺麻煩的,因為我對這里還不是特別清楚,所以也就先不說了,最終這個選課系統(tǒng)的后臺服務(wù)器還是被重啟了,這個時候再次嘗試的時候...一個字爽,暢快的感覺,總的來說我覺得一個后端項目如果不能保證并發(fā)量的出現(xiàn)能夠正常運(yùn)行,給我感覺是個失敗的項目

    應(yīng)用服務(wù)器宕機(jī)

    應(yīng)用服務(wù)器為什么會宕機(jī)?例如Tomcat來說,其中的Connector組件維護(hù)一個線程池,一條新的請求到達(dá)服務(wù)器的時候,簡單地來說就是一條線程去處理一條請求,這個在SpringBoot項目或者SSM項目中基于J2EE規(guī)范的后端服務(wù)中l(wèi)og打的全的話可以觀察到,處理請求和響應(yīng)其實是同一個線程,如果服務(wù)器采用同步方式去處理請求,這個時候大家都知道I/O的效率是很低的,如果說一條請求需要處理一條很費(fèi)時的I/O操作,也就是說這次請求需要占用這個這個線程直到它執(zhí)行完I/O操作,使用過Tomcat應(yīng)用服務(wù)器的人應(yīng)該都知道,線程池也是有默認(rèn)最高上限的,調(diào)得過高可能會影響線程池的工作,低了可能并發(fā)量比較低,我一直用的默認(rèn)的沒有去管過

    <Connector port="8080"    
                   maxThreads="150" minSpareThreads="25" maxSpareThreads="75"    
                   enableLookups="false" redirectPort="8443" acceptCount="100"    
                   debug="0" connectionTimeout="20000"     
                   disableUploadTimeout="true" />    
    

    這個是Tomcat conf下server.conf文件的配置,需要了解的可以去試試,這個時候同步策略處理請求,一旦占用時間過長,例如部署了一個并發(fā)量較高的服務(wù),請求峰值一旦來臨,線程池將會被耗盡,并且可能造成整個應(yīng)用服務(wù)器的宕機(jī),當(dāng)然處理這種邏輯,我們可以在代碼中使用異步處理請求來實現(xiàn)

    同步服務(wù)為每個請求創(chuàng)建單一線程,由此線程完成整個請求的處理:接收消息,處理消息,返回數(shù)據(jù);這種情況下服務(wù)器資源對所有請求開放,服務(wù)器資源被所有入棧請求競爭使用,如果請求過多就會導(dǎo)致服務(wù)器資源耗盡宕機(jī),或者導(dǎo)致競爭加劇,資源調(diào)度頻繁,服務(wù)器資源利用效率降低。

    來降低web服務(wù)器的負(fù)擔(dān),并且還能夠響應(yīng)并發(fā)量較大的情況,綜上所述,為了能夠配置一個高并發(fā)量的后端架構(gòu),最好是項目后端架構(gòu)轉(zhuǎn)向集群

  • 要是讓我做這個選課系統(tǒng)我會如何架構(gòu)

    首先考慮到并發(fā)量,因為其實實現(xiàn)一個服務(wù)來說很簡單,主要就是并發(fā)量較大的情況下,服務(wù)器能不能承受住這種壓力正常地運(yùn)轉(zhuǎn),限時選課系統(tǒng)如果不作處理很難保證在后端運(yùn)行的時候不會出現(xiàn)響應(yīng)過慢甚至宕機(jī)的情況,我還是選擇Nginx作為負(fù)載均衡服務(wù)器,因為官方給定的Nginx訪問的并發(fā)量最高能到5W,可是我看過實際測試也就只能到3W,但是對于我們這個系統(tǒng)..完全夠了,其次就是Tomcat的集群,項目使用SpringBoot搭建,驗證碼以及SSO處理邏輯會使用到Redis這種NoSql數(shù)據(jù)庫,如果一旦使用到數(shù)據(jù)庫,最好還是做數(shù)據(jù)庫的集群,主從庫的建立,Redis的集群以及主從庫設(shè)置可以看我上一篇博客,MySql的集群搭建,主從庫的建立,MySql這里我沒有嘗試過搭建集群,所以也不再贅述,如果使用Nginx負(fù)載均衡去配合應(yīng)用服務(wù)器的集群的話,即使是應(yīng)用服務(wù)器集群中的某一臺宕機(jī),也不會影響到別的服務(wù)器運(yùn)行也不會影響業(yè)務(wù)

項目架構(gòu)演進(jìn)示意圖

后端項目架構(gòu)演進(jìn)

集群產(chǎn)生的問題

Cookie Session策略實現(xiàn)登錄邏輯

試想一下這個場景,后端采用Tomcat集群,有5臺Tomcat,配置Nginx作為負(fù)載均衡服務(wù)器,采用權(quán)重策略進(jìn)行反向代理,假如Nginx將一個用戶的請求首先轉(zhuǎn)發(fā)到了Tomcat1上,用戶進(jìn)行了登錄,響應(yīng)中可以拿到cookie或者set-cookie字段,并且value若是基于Tomcat應(yīng)用服務(wù)器的話,value的值基本都是JSESSION=xxxxxxxx類似的情況,Tomcat底層維護(hù)著一個Map,通過這個JSESSIONID尋找屬于用戶與服務(wù)器之間的會話,并get到session對象,就可以實現(xiàn)訪問放置在session中的一些用戶信息或一些其余別的放置在session中的敏感信息

問題出現(xiàn)

cookie session策略用于解決Http無狀態(tài)的問題,但是如果集群Tomcat之后,用戶如果登錄請求被Nginx轉(zhuǎn)發(fā)到了Tomcat1上,并且做了登錄,那么這個cookie默認(rèn)情況下會被保存至瀏覽器的緩存中,直至一次瀏覽器的生命周期結(jié)束cookie將被銷毀,但是這個cookie所對應(yīng)的session會話也只是針對于對客戶端/Web與Tomcat1之間,用戶登錄了,那么之后呢?

如果用戶接下來訪問個人信息頁,這個時候假如配置Nginx的負(fù)載均衡策略為權(quán)重策略,并且5臺Tomcat的權(quán)重相同(轉(zhuǎn)發(fā)到每一臺的幾率都相同,還有ip hash等等一些策略去實現(xiàn)負(fù)載均衡,這里也不贅述),如果訪問個人信息這個請求被Nginx轉(zhuǎn)發(fā)到了除Tomcat1之外的任意一臺服務(wù)器,都會出現(xiàn)一個問題,這個問題是什么大家都可以想一想

繼續(xù)要求登錄

因為請求個人信息這個請求攜帶的cookie并不能標(biāo)示Tomcat2上的一次會話,想來也很清楚,這個用戶根本沒在Tomcat2上做過登錄,那這樣的話集群帶來的代價有點高,這樣的話如果集群的規(guī)模比較大,也就是說有可能后來訪問任何需要驗證登錄的接口都會判斷為未登錄,這種情況只要不解決session共享問題,那么都會出現(xiàn)問題

如何解決session共享 實現(xiàn)SSO

github: https://github.com/challengerzsz/Mall 項目可以參考一下

  • 貼上一個簡單的用戶登錄Controller,在登錄邏輯中,若用戶登錄成功,則使用封裝的Cookie工具操作,實例化一個Cookie對象,并且設(shè)置時長以及domain參數(shù)(為了讓這個cookie在請求二級域名的時候可以獲取到),還有一些設(shè)置都可以自行百度,在代碼中設(shè)置的超時時間為1年,可以根據(jù)自己的邏輯來使用,最后向響應(yīng)中加入這個Cookie,Cookie中的key為一個常量,value為登錄這次請求的會話sessionId

    /**
     * 用戶登錄
     *
     * @param username
     * @param password
     * @return
     */
    @PostMapping("/login")
    public ServerResponse<User> login(String username, String password, HttpSession session, HttpServletResponse response) {
    
        ServerResponse<User> serverResponse = userService.login(username, password);
        if (serverResponse.isSuccess()) {
            CookieUtil.writeLoginToken(response, session.getId());
            redisUtil.setRedisValueEx(session.getId(),    JsonUtil.objToString(serverResponse.getData()),
                        Const.RedisCacheExTime.REDIS_SESSION_EXTIME);
            }
            return serverResponse;
    }
    
    public static void writeLoginToken(HttpServletResponse response, String token) {
    
            Cookie cookie = new Cookie(COOKIE_NAME, token);
            cookie.setDomain(COOKIE_DOMAIN);
            //設(shè)置cookie的path為/ 這樣二級域名可以共享到最大域名下的cookie實現(xiàn)共享
            cookie.setPath("/");
          //通過腳本將無法讀取到Cookie信息,避免腳本攻擊
            cookie.setHttpOnly(true);
            //若不設(shè)置cookie的有效期 生命周期為瀏覽器的生命周期 在內(nèi)存不會持久化到硬盤
            cookie.setMaxAge(60 * 60 * 24 * 365);
            logger.info("write cookieName :{}, cookieValue :{}", cookie.getName(), cookie.getValue());
            response.addCookie(cookie);
    }
    
  • 其實大家能夠看出來,這種解決session共享的問題是通過我們強(qiáng)行向瀏覽器寫入一個新Cookie,規(guī)定這個Cookie中的key為一個聲明的常量標(biāo)示這個Cookie,value為首次登錄請求的那一次會話中,應(yīng)用服務(wù)器返回給瀏覽器的sessionId,當(dāng)然這個Cookie只會在登錄成功的邏輯下才會被回寫回響應(yīng)

  • 調(diào)用Cookie工具校驗是否登錄

    大家應(yīng)該已經(jīng)猜到封裝的Cookie工具要實現(xiàn)什么了,所有訪問需要身份驗證的接口都應(yīng)該調(diào)用這個工具類,首先從請求中取出Cookie,這里要強(qiáng)調(diào)一下,取出的Cookie如果做過登錄操作,那么應(yīng)該有兩個Cookie,一個是Tomcat1自己返回給瀏覽器的Cookie,另一個是我們手動寫入的一個Cookie,通過校驗是否存在有我們手寫的這個Cookie,進(jìn)而判斷用戶是否已經(jīng)完成過登錄 ,這樣就完了嗎?大家可以想一想,這個時候如果知道了服務(wù)端手寫的Cookie的key就可以偽造一個Cookie去進(jìn)行請求,那么如果校驗邏輯真的就這樣的話,我們?nèi)绾未_保這個用戶是我們的用戶,并且是登錄后訪問的我們的服務(wù)?

  • HttpOnly

    大家應(yīng)該可以看到上面代碼段有設(shè)置cookie屬性的語句

    cookie.setHttpOnly(true);
    

    這句話是什么意思呢?

    如果cookie中設(shè)置了HttpOnly屬性,那么通過js腳本將無法讀取到cookie信息,這樣能有效的防止XSS攻擊,竊取cookie內(nèi)容,這樣就增加了cookie的安全性,即便是這樣,也不要將重要信息存入cookie。

    XSS全稱Cross SiteScript,跨站腳本攻擊,是Web程序中常見的漏洞,XSS屬于被動式且用于客戶端的攻擊方式,所以容易被忽略其危害性。其原理是攻擊者向有XSS漏洞的網(wǎng)站中輸入(傳入)惡意的HTML代碼,當(dāng)其它用戶瀏覽該網(wǎng)站時,這段HTML代碼會自動執(zhí)行,從而達(dá)到攻擊的目的。如,盜取用戶Cookie、破壞頁面結(jié)構(gòu)、重定向到其它網(wǎng)站等。

    也就是說cookie通過設(shè)置這一參數(shù)為true則可以實現(xiàn)防止腳本偽造cookie進(jìn)行攻擊,但是這樣后端就不需要校驗了嗎?我在有的網(wǎng)站也看到了HttpOnly這種安全措施有的時候也不安全的說法,那么我們?nèi)绾稳プ瞿?/p>

    Redis的參與

    細(xì)心的人應(yīng)該已經(jīng)看到上面UserController登錄中有一句代碼

  redisUtil.setRedisValueEx(session.getId(),    JsonUtil.objToString(serverResponse.getData()),
                      Const.RedisCacheExTime.REDIS_SESSION_EXTIME);

這句話是什么意思呢,我封裝了一個對RedisTemplate操作的工具類,通過使用RedisTemplate操作Redis,并且設(shè)置鍵值攜帶過期屬性,Redis中的key為登錄時會話session的Id,值為將此用戶的實例通過封裝好的JsonUtil進(jìn)行序列化后的Json字符串,最終以字符串的形式作為key保存在Redis中

工具類讀取Cookie校驗的時候,如果有我們手寫的Cookie并且有value的情況下,通過調(diào)用redis中的get方法去校驗這個sessionId是否是登錄是我們set進(jìn)Redis中的值,如果能夠從Redis中通過這個sessionId能夠get到用戶的Json數(shù)據(jù),也就說明確實登錄過也就防止了偽造,如需使用用戶信息的時候,將這個Json字符串反序列化成為實例對象即可

封裝CookieUtil讀取Cookie的方法

  
  /**
   * 獲取屬于mall服務(wù)器下的cookie 并且返回cookie的值即登錄時的sessionId
   * @param request
   * @return
   */
  public static String readLoginToken(HttpServletRequest request
          Cookie[] cookies = request.getCookies();
          if (cookies != null) {
              for (Cookie cookie : cookies) {
                  logger.info("read cookieName :{} cookieValue :{}", cookie.getName(), cookie.getValue());
                  if (StringUtils.equals(cookie.getName(), COOKIE_NAME)) {
                      logger.info("return cookieName :{} cookieValue :{}", cookie.getName(), cookie.getValue());
                      return cookie.getValue();
                  }
              }
          }
          return null;
  }

調(diào)用需要校驗身份信息的借口時可以這樣來操作

@GetMapping("/getInfo")
public ServerResponse<User> getInfo(HttpServletRequest request) {

    String loginToken = CookieUtil.readLoginToken(request);
    logger.error("error {}", loginToken);
    if (StringUtils.isEmpty(loginToken)) {
        return ServerResponse.createByErrorMsg("用戶未登錄");
    }
    String userJson = redisUtil.getRedisValue(loginToken);
    User currentUser = JsonUtil.stringToObj(userJson, User.class);
    if (currentUser == null) {
        return ServerResponse.createByErrorCodeMsg(ResponseCode.NEED_LOGIN.getCode(), "未登錄,需要強(qiáng)制登錄");
    return userService.getInfo(currentUser.getId());
}

如果博客中有問題,請私信我一同解決

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容