Session機制詳解及分布式中Session共享解決方案


一、為什么要產生Session

  http協議本身是無狀態的,客戶端只需要向服務器請求下載內容,客戶端和服務器都不記錄彼此的歷史信息,每一次請求都是獨立的。

  為什么是無狀態的呢?因為瀏覽器與服務器是使用socke套接字進行通信,服務器將請求結果返回給瀏覽器之后,會關閉當前的socket鏈接,而且服務器也會在處理頁面完畢之后銷毀頁面對象。

  然而在Web應用的很多場景下需要維護用戶狀態才能正常工作(是否登錄等),或者說提供便捷(記住密碼,瀏覽歷史等),狀態的保持就是一個很重要的功能。因此在web應用開發里就出現了保持http鏈接狀態的技術:一個是cookie技術,另一種是session技術。

二、Session有什么作用,如何產生並發揮作用

  要明白Session就必須要弄明白什么是Cookie,以及Cookie和Session的關系。

  1、什么是Cookie

  Cookie技術是http狀態保持在客戶端的解決方案,Cookie就是由服務器發給客戶端的特殊信息,而這些信息以文本文件的方式存放在客戶端,然后客戶端每次向服務器發送請求的時候都會帶上這些特殊的信息。

  2、Cookie的產生

  當用戶首次使用瀏覽器訪問一個支持Cookie的網站的時候,用戶會提供包括用戶名在內的個人信息並且提交至服務器;接着,服務器在向客戶端回傳相應的超文本的同時也會發回這些個人信息,當然這些信息並不是存放在HTTP響應體(Response Body)中的,而是存放於HTTP響應頭(Response Header);當客戶端瀏覽器接收到來自服務器的響應之后,瀏覽器會將這些信息存放在一個統一的位置。

  存儲在硬盤上的cookie 不可以在不同的瀏覽器間共享,可以在同一瀏覽器的不同進程間共享,比如兩個IE窗口。這是因為每中瀏覽器存儲cookie的位置不一樣,比如

  Chrome下的cookie放在:C:\Users\sharexie\AppData\Local\Google\Chrome\User Data\Default\Cache

  Firefox下的cookie放在:C:\Users\sharexie\AppData\Roaming\Mozilla\Firefox\Profiles\tq2hit6m.default\cookies.sqlite (倒數第二個文件名是隨機的文件名字)

  Ie下的cookie放在:C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Cookies

  3、Cookie的內容、作用域以及有效期

  cookie的內容主要包括:名字,值,過期時間,路徑和域。路徑與域合在一起就構成了cookie的作用范圍。

  如果不設置過期時間,則表示這個cookie的生命期為瀏覽器會話期間,只要關閉瀏覽器窗口,cookie就消失了,這種生命期為瀏覽器會話期的 cookie被稱為會話cookie。會話cookie一般不存儲在硬盤上而是保存在內存里。如果設置了過期時間,瀏覽器就會把cookie保存到硬盤上,關閉后再次打開瀏覽器,這些cookie仍然有效直到超過設定的過期時間。

  4、Cookie如何使用

  cookie 的使用是由瀏覽器按照一定的原則在后台自動發送給服務器的。

  當客戶端二次向服務器發送請求的時候,瀏覽器檢查所有存儲的cookie,如果某個cookie所聲明的作用范圍大於等於將要請求的資源所在的位置,則把該cookie附在請求資源的HTTP請求頭上發送給服務器。有了Cookie這樣的技術實現,服務器在接收到來自客戶端瀏覽器的請求之后,就能夠通過分析存放於請求頭的Cookie得到客戶端特有的信息,從而動態生成與該客戶端相對應的內容。通常,我們可以從很多網站的登錄界面中看到“請記住我”這樣的選項,如果你勾選了它之后再登錄,那么在下一次訪問該網站的時候就不需要進行重復而繁瑣的登錄動作了,而這個功能就是通過Cookie實現的。

  5、什么是Session

  Session一般叫做回話,Session技術是http狀態保持在服務端的解決方案,它是通過服務器來保持狀態的。我們可以把客戶端瀏覽器與服務器之間一系列交互的動作稱為一個 Session。是服務器端為客戶端所開辟的存儲空間,在其中保存的信息就是用於保持狀態。因此,session是解決http協議無狀態問題的服務端解決方案,它能讓客戶端和服務端一系列交互動作變成一個完整的事務。

  6、Session的創建

  那么Session在何時創建呢?當然還是在服務器端程序運行的過程中創建的,不同語言實現的應用程序有不同創建Session的方法。

  當客戶端第一次請求服務端,當server端程序調用 HttpServletRequest.getSession(true)這樣的語句時的時候,服務器會為客戶端創建一個session,並將通過特殊算法算出一個session的ID,用來標識該session對象。

  Session存儲在服務器的內存中(tomcat服務器通過StandardManager類將session存儲在內存中),也可以持久化到file,數據庫,memcache,redis等。客戶端只保存sessionid到cookie中,而不會保存session。

  瀏覽器的關閉並不會導致Session的刪除,只有當超時、程序調用HttpSession.invalidate()以及服務端程序關閉才會刪除。

  7、Tomcat中的Session創建

  ManagerBase是所有session管理工具類的基類,它是一個抽象類,所有具體實現session管理功能的類都要繼承這個類,該類有一個受保護的方法,該方法就是創建sessionId值的方法:

(tomcat的session的id值生成的機制是一個隨機數加時間加上jvm的id值,jvm的id值會根據服務器的硬件信息計算得來,因此不同jvm的id值都是唯一的)。
  StandardManager類是tomcat容器里默認的session管理實現類,它會將session的信息存儲到web容器所在服務器的內存里。
  PersistentManagerBase也是繼承ManagerBase類,它是所有持久化存儲session信息的基類,PersistentManager繼承了PersistentManagerBase,但是這個類只是多了一個靜態變量和一個getName方法,目前看來意義不大,對於持久化存儲session,tomcat還提供了StoreBase的抽象類,它是所有持久化存儲session的基類,另外tomcat還給出了文件存儲FileStore和數據存儲JDBCStore兩個實現。

  8、Cookie與Session的關系

  cookie和session的方案雖然分別屬於客戶端和服務端,但是服務端的session的實現對客戶端的cookie有依賴關系的,服務端執行session機制時候會生成session的id值,這個id值會發送給客戶端,客戶端每次請求都會把這個id值放到http請求的頭部發送給服務端,而這個id值在客戶端會保存下來,保存的容器就是cookie,因此當我們完全禁掉瀏覽器的cookie的時候,服務端的session也會不能正常使用。

三、分布式系統中Session共享問題

  其實就是服務器集群Session共享的問題,在客戶端與服務器通訊會話保持過程中,Session記錄整個通訊的會話基本信息。但是在集群環境中,假設客戶端第一次訪問服務A,服務A響應返回了一個sessionId並且存入了本地Cookie中。第二次不訪問服務A了,轉去訪問服務B。因為客戶端中的Cookie中已經存有了sessionId,所以訪問服務B的時候,會將sessionId加入到請求頭中,而服務B因為通過sessionId沒有找到相對應的數據,因此它就會創建一個新的sessionId並且響應返回給客戶端。這樣就造成了不能共享Session的問題。

  例如在SpringCloud項目中,啟動一個服務,分別用兩個不同的端口,然后在Eureka Server中注冊,那么這樣就形成了兩台服務的集群,Ribbon的負載均衡策略設置為輪詢策略。如服務端處理請求為:

@RestController
public class TestSessionController {

    @Value("${server.port}")
    private Integer projectPort;// 項目端口

    @RequestMapping("/createSession")
    public String createSession(HttpSession session, String name) {
        session.setAttribute("name", name);
        return "當前項目端口:" + projectPort + " 當前sessionId :" + session.getId() + "在Session中存入成功!";
    }

    @RequestMapping("/getSession")
    public String getSession(HttpSession session) {
        return "當前項目端口:" + projectPort + " 當前sessionId :" + session.getId() + "  獲取的姓名:" + session.getAttribute("name");
    }

}

  我們直接通過輪詢機制來訪問首先向Session中存入一個姓名,http://www.hello.com/createSession?name=AAA

當前項目端口:8081 當前sessionId :0F20F73170AE6780B1EC06D9B06210DB在Session中存入成功!

  因為我們使用的是默認的輪詢機制,那么下次肯定訪問的是8080端口,我們直接獲取以下剛才存入的值http://www.hello.com/getSession

當前項目端口:8080 當前sessionId :C6663EA93572FB8DAE27736A553EAB89 獲取的姓名:null

  發現8080端口中並沒有我們存入的值,並且sessionId也是與8081端口中的不同。

  因為輪詢機制這個時候我們是8081端口的服務器,那么之前我們是在8081中存入了一個姓名,8080端口的服務端並沒有存入Session。

  那么我們再次訪問8081端口服務看看是否能夠獲取到我們存入的姓名:AAA,繼續訪問:http://www.hello.com/getSession

當前項目端口:8081 當前sessionId :005EE6198C30D7CD32FBD8B073531347 獲取的姓名:null

  發現不但8080端口沒有,連之前存入過的8081端口也沒有了。

  其實發現第三次訪問8081的端口sessionid都不一樣了,是因為我們在第二次去訪問的時候訪問的是8080端口這個時候客戶端在cookie中獲取8081的端口去8080服務器上去找,沒有找到后重新創建了一個session並且將sessionid響應給客戶端,客戶端又保持到cookid中替換了之前8081的sessionid,那么第三次訪問的時候拿着第二次訪問的sessionid去找又找不到然后又創建。一直反復循環,兩個服務器永遠拿到的是對方生成的sessionId,拿不到自己生成的sessionId,這就是集群中Session共享問題。

四、如何解決Session共享問題

  常見Session共享方案有如下幾種:

  • 使用cookie來完成(很明顯這種不安全的操作並不可靠)
  • 使用Nginx中的ip綁定策略,同一個ip只能在指定的同一個機器訪問(不支持負載均衡)
  • 使用數據庫同步session(效率不高)
  • 使用tomcat內置的session同步(同步可能會產生延遲)
  • 使用token代替session
  • 使用Spring-Session+Redis實現

  1、使用Cookie實現

  這個方式原理是將系統用戶的Session信息加密、序列化后,以Cookie的方式, 統一種植在根域名下(如:.host.com),利用瀏覽器訪問該根域名下的所有二級域名站點時,會傳遞與之域名對應的所有Cookie內容的特性,從而實現用戶的Cookie化Session在多服務間的共享訪問。

  這個方案的優點無需額外的服務器資源;缺點是由於受http協議頭信息長度的限制,僅能夠存儲小部分的用戶信息,同時Cookie化的 Session內容需要進行安全加解密(如:采用DES、RSA等進行明文加解密;再由MD5、SHA-1等算法進行防偽認證),另外它也會占用一定的帶寬資源,因為瀏覽器會在請求當前域名下任何資源時將本地Cookie附加在http頭中傳遞到服務器,最重要的是存在安全隱患

  2、使用Nginx中的ip綁定策略

  這個只需要在Nginx中簡單配置一句 ip_hash; 就可以了,但是該方式的缺點也很明顯,配置了IP綁定就不支持Nginx的負載均衡了。具體可以參考博客:https://www.cnblogs.com/ywb-articles/p/10686673.html

  3、使用數據庫同步session

  以為MySQL為例,每次將session數據存到數據庫中。這個方案還是比較可行的,不少開發者使用了這種方式。但它的缺點在於Session的並發讀寫能力取決於MySQL數據庫的性能,對數據庫的壓力大,同時需要自己實現Session淘汰邏輯,以便定時從數據表中更新、刪除 Session記錄,當並發過高時容易出現表鎖,雖然可以選擇行級鎖的表引擎,但很多時候這個方案不是最優方案。

  4、使用token代替session

  這里token是JSON Web Token,一般用它來替換掉Session實現數據共享。

  使用基於 Token 的身份驗證方法,在服務端不需要存儲用戶的登錄記錄。大概的流程是這樣的: 

      1、客戶端通過用戶名和密碼登錄服務器;
      2、服務端對客戶端身份進行驗證;
      3、服務端對該用戶生成Token,返回給客戶端;
      4、客戶端將Token保存到本地瀏覽器,一般保存到cookie中;
      5、客戶端發起請求,需要攜帶該Token;
      6、服務端收到請求后,首先驗證Token,之后返回數據。

        

  如上圖,左圖為Token實現方式,有圖為session實現方式,流程大致一致。具體可參考:https://www.cnblogs.com/lightzone/p/9749076.html

  瀏覽器第一次訪問服務器,根據傳過來的唯一標識userId,服務端會通過一些算法,如常用的HMAC-SHA256算法,然后加一個密鑰,生成一個token,然后通過BASE64編碼一下之后將這個token發送給客戶端;客戶端將token保存起來,下次請求時,帶着token,服務器收到請求后,然后會用相同的算法和密鑰去驗證token,如果通過,執行業務操作,不通過,返回不通過信息。

  優點:

  • 無狀態、可擴展 :在客戶端存儲的Token是無狀態的,並且能夠被擴展。基於這種無狀態和不存儲Session信息,負載均衡器能夠將用戶信息從一個服務傳到其他服務器上。
  • 安全:請求中發送token而不再是發送cookie能夠防止CSRF(跨站請求偽造)。
  • 可提供接口給第三方服務:使用token時,可以提供可選的權限給第三方應用程序。
  • 多平台跨域

  對應用程序和服務進行擴展的時候,需要介入各種各種的設備和應用程序。 假如我們的后端api服務器a.com只提供數據,而靜態資源則存放在cdn 服務器b.com上。當我們從a.com請求b.com下面的資源時,由於觸發瀏覽器的同源策略限制而被阻止。

  我們通過CORS(跨域資源共享)標准和token來解決資源共享和安全問題。

  舉個例子,我們可以設置b.com的響應首部字段為:

Access-Control-Allow-Origin: http://a.com

Access-Control-Allow-Headers: Authorization, X-Requested-With, Content-Type, Accept

Access-Control-Allow-Methods: GET, POST, PUT,DELETE

  第一行指定了允許訪問該資源的外域 URI。

  第二行指明了實際請求中允許攜帶的首部字段,這里加入了Authorization,用來存放token。

  第三行用於預檢請求的響應。其指明了實際請求所允許使用的 HTTP 方法。

  然后用戶從a.com攜帶有一個通過了驗證的token訪問B域名,數據和資源就能夠在任何域上被請求到。

  5、使用tomcat內置的session同步

  5.1 tomcat中server.xml直接配置

<!-- 第1步:修改server.xml,在Host節點下添加如下Cluster節點 -->
<!-- 用於Session復制 -->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8">
    <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true" />
    <Channel className="org.apache.catalina.tribes.group.GroupChannel">
        <Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" 
                    port="45564" frequency="500" dropTime="3000" />
        <!-- 這里如果啟動出現異常,則可以嘗試把address中的"auto"改為"localhost" -->
        <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="4000" 
                  autoBind="100" selectorTimeout="5000" maxThreads="6" />
        <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
            <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
        </Sender>
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor" />
    </Channel>
    <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter="" />
    <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
    <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer" tempDir="/tmp/war-temp/" 
              deployDir="/tmp/war-deploy/" watchDir="/tmp/war-listen/" watchEnabled="false" />
    <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener" />
</Cluster>

  5.2 修改web.xml

  web.xml中需加入<distributable/> 以支持集群。

<distributable/>

  5、Spring-Session+Redis實現

  Spring提供了一個解決方案:Spring-Session用來解決兩個服務之間Session共享的問題。

  5.1 在pom.xml中添加相關依賴

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring session 與redis應用基本環境配置,需要開啟redis后才可以使用,不然啟動Spring boot會報錯 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

  5.2 修改application.properties全局配置文件(本地要開啟redis服務)

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
#spring.redis.password=
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=8
spring.redis.timeout=10000

  5.3 在代碼中添加Session配置類

/**
 * 這個類用配置redis服務器的連接
 * maxInactiveIntervalInSeconds為SpringSession的過期時間(單位:秒)
 */
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
    // 冒號后的值為沒有配置文件時,制動裝載的默認值
    @Value("${redis.hostname:localhost}")
    private String hostName;
    @Value("${redis.port:6379}")
    private int port;
    // @Value("${redis.password}")
    // private String password;

    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration =
                new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(hostName);
        redisStandaloneConfiguration.setPort(port);
        // redisStandaloneConfiguration.setDatabase(0);
        // redisStandaloneConfiguration.setPassword(RedisPassword.of("123456"));
        return new JedisConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

  5.4 初始化Session配置

/**
 * 初始化Session配置
 */
public class RedisSessionInitializer extends AbstractHttpSessionApplicationInitializer {
    public RedisSessionInitializer() {
        super(RedisSessionConfig.class);
    }
}

  Spring-Sesion實現的原理

  當Web服務器接收到請求后,請求會進入對應的Filter進行過濾,將原本需要由Web服務器創建會話的過程轉交給Spring-Session進行創建Spring-Session會將原本應該保存在Web服務器內存的Session存放到Redis中。然后Web服務器之間通過連接Redis來共享數據,達到Sesson共享的目的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM