Spring-Session 知識點 和 源碼分析(上)

一、什麼是 session ?  每一個用戶 只擁有一個 session 。

(1)Session 的 作用 就是 讓我們的 服務器記住 是哪個用戶,也就是瀏覽器訪問過我。當我發起一個 會話,我們的服務器端 就會 爲我們產生 session,每一個session對應一個sessionID,然後將這個sessionID存放在cookie中然後將cookie存放在我們的瀏覽器中,然後我們在不關閉瀏覽器的 條件下,每次發送請求的時候,會默認的無條件的 將我們的 對應這個服務器的 cookie 重新發送給我們的 服務器,這樣我們的 服務器就重新難道了 存放在cookie中的sessionID,從而就找到了對應的session,這樣就實現了服務器記住我們用戶。 我們 在 很多 情況 都會 使用過 cookie 來跟蹤 我們的 session,用cookie來存放 sessionID實現跟蹤。

 

每一個用戶(瀏覽器) 都對應一個 唯一的獨有的 sessionID,通過這個sessionID可以找到唯一對應的 session,我們瀏覽器的cookie中保存着 第一次 會話 由 服務器傳遞過來的sessionID。

 

【URL重寫技術 解決 我們的session追蹤問題】:

如果 我們的 瀏覽器 禁止 了 向 cookie 中存放數據,那麼我們的 服務器 就沒有辦法 向我們的 cookie中存放 sessionID,那樣我們怎樣實現 對 session的 跟蹤 ?

解答: 這裏我們使用到了一個 【URL重寫】技術 。

  這個技術 非常好理解: 就是在我們每次發送請求的時候,我們的瀏覽器會將從服務器哪裏接收到的 sessionID 當做一個 請求參數 掛在請求的 最後,如:

Http://localhost:8080/myweb/index.jsp?sessionId=xxxxx 。

這個參數 在我們的 服務器端 會 被接收,我們服務器拿到這個參數之後,用來查找判斷這個sessionId對應哪個session。

 

【session 的管理】

(1)session 通常是 交給我們的 tomcat容器管理的。因爲我們的tomcat是用來處理動態資源的,所以我們得tomcat中的一個容器專門用來存放我們的 session 。

(2)但是我們的 實際開發中我們不單單隻涉及一個 tomcat服務器了,我們是多個tomcat服務器集羣的,而且 我們用戶和tomcat之間還存在一個nginx,這個nginx是用來做 負載均衡的。這時候就出現了一個問題:

我們的 nginx負載均衡如果不做任何的配置,nginx接收到的請求分發到的tomcat可能是不相同的,因爲nginx是根據tomcat的壓力和一定的規則來進行分發請求的,所以我們用戶 每次發出的請求 處理請求的 tomcat不一定是相同的。

但是我們的 每一個tomcat中都部署 相同的 web項目,所以我們的 用戶 在每次 訪問我們的頁面的時候 雖然是不同的 tomcat進行處理,但是顯示的網站是相同 的。

 

因爲每次 處理請求的tomcat 是不相同的,就會造成一個問題:session的丟失和不能共享的問題:

因爲每次由nginx負載均衡的原因分到的tomcat不相同,所以每次都找不到上一次請求會話sessionId對應的session,所以導致這次的tomcat認爲這個請求是新的請求,也是新的一次會話,所以產生新的 session,然後返回新的sessionId,然後代替掉瀏覽器中的cookie中的sessionId,這樣就 每次 用戶登錄之後 發出一個請求然後刷新一下頁面就退出了,就變成了登錄又退出登錄退出這樣的情況。 這就是tomcat集羣部署之後存在的問題。

 

****我們怎樣解決tomcat集羣部署和nginx負載均衡產生的 session不共享丟失的問題呢?

 

【第一種方案】:ip_hash策略:這個策略是 我們nginx負載均衡時在nginx.Conf配置文件下配置的 請求分配策略。

 Ip_hash的原理: 一個ip地址綁定一個tomcat服務器。其中涉及一個計算公式:

Hash函數(  ip地址 )  %  2  == 取餘,餘數對應tomcat的編號。

所以只要ip地址不變 ,對應的 tomcat編號就不變。

但是 這種 解決 session丟失問題的 方法不好:因爲 這樣我們的ip地址就不能變了,如果我們今天從山東用的山東移動,明天去了河北,用的河北移動,這樣session就沒了,意思是ip地址不能改變的,如果你ip變了,就有可能調到了另一個服務器中去了,左腳在北京 右腳進了河北,ip變化了,然後跳轉tomcat,然後用戶就丟失了session,就退出登錄了。

 

【第二種方案】:Spring Session 框架。

這種技術 可以使 session 同步 ,可以對session進行管理,解決集羣部署之後 產生的session丟失不同步的 問題。

*****它是怎麼實現的?

我們的spring session 以中立的身份,取代和代替 我們 tomcat中的 httpsession,然後將session中的信息數據 存放在 redis 中。

知識點:爲什麼存放在redis中呢?

我們的redis是將數據存放在內存中的,而且我們可以給數據設置存放的最大生命時間,達到最大生命時間之後就會被自動刪除,這一點和我們的session的最大空閒時間是相同相似的。而且我們從內存中讀取數據是很快的

作用步驟:

(1)用自身的 Spring-Session 以中立的方式 替換我們的 http-Session。

(2)然後 將 session中的數據信息 存放進 redis 中 。

(3)只需要配置 不需要 寫任何的代碼,就可以實現springsesson功能。

開發步驟:使用springsession開發步驟。

1.添加springsession的 maven 依賴。 
1)
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>1.3.1.RELEASE</version>
</dependency>
這個依賴 是對我們的 spring-data-redis依賴的封裝。這是依賴是我們springsession所依賴的jar包 。 
2)
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.8.8.RELEASE</version>
</dependency>
這個依賴 是我們 在 spring框架中 【操作redis】時 所需要的依賴。
3)
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
我們的【spring-data-redis】這個依賴 在操作 redis的時候,底層所依賴的jar包是jedis包,實際上 我們【spring-data-redis】包底層是通過【jedis】包來操作我們的redis的。
4)
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>4.3.13.RELEASE</version>
</dependency>
我們的spring-session是在web項目中使用,所以涉及到了web項目和spring,所以需要使用【spring-web】這個依賴。
5)我們在開發web項目時,所需要的一些依賴,可加可不加,加了有提示不報錯。
<!-- servlet依賴的jar包start -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
</dependency>
<!-- servlet依賴的jar包start -->

<!-- jsp依賴jar包start -->
<dependency>
    <groupId>javax.servlet.jsp</groupId>
    <artifactId>javax.servlet.jsp-api</artifactId>
    <version>2.3.1</version>
</dependency>
<!-- jsp依賴jar包end -->

<!--jstl標籤依賴的jar包start -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
<!--jstl標籤依賴的jar包end -->
分別是【servlet依賴】,【jsp依賴】 和 【jstl依賴】。
2.我們在web.xml文件文件中配置springSessionRespositoryFilter過濾器。
SpringSessionRespositoryFilter:springSession倉庫過濾器。
我認爲這個過濾器的作用是:將 我們 所有的 httpSession替換成 springsession,然後將我們session中的信息數據存放在redis中。 

applicationContext-session.xml配置文件內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- spring註解、bean的處理器 -->
    <!--這個標籤 是用來 激活註解 的-->
    <context:annotation-config/>

    <!-- Spring session 的配置類 -->
    <!--RedisHttpSessionConfiguration這個類專門是用來處理 redis 和 httpsession之間的關係的類-->
    <!--
           我們的RedisHttpSessionConfiguration 這個類中 有很多很多的註解,但是 RedisHttpSessionConfiguration裏面的這些方法上類上的註解並沒有被激活。
           所以我們需要  <context:annotation-config/> 這個標籤激活一下 我們 我們的RedisHttpSessionConfiguration類中的註解。
           如果我們不進行 註解激活,那麼我們的RedisHttpSessionConfiguration是不能使用的,會報錯。
    -->
    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>


    <!--配置 redis的連接時  所需要的 信息 的-->


    <!--
    spring 封裝了 RedisTemplate 對象來進行對redis的各種操作,它支持所有的 redis 原生的 api。
    -->


    <!--
         讀取redis.properties屬性配置文件
         在我們的 applictionContext-session.xml中引入redis的配置文件,然後就可以使用EL表達式進行賦值了。
    -->
    <context:property-placeholder location="classpath:redis.properties"/>


    <!--下面的配置是爲了告訴我們的springsession怎麼連接redis的,只需要告訴我們的springsession怎麼連接redis,我們都不需要創建RedisTemplate 對象 -->
    <!-- 配置jedis連接工廠,用於連接redis -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="${redis.hostName}"/>
        <property name="port" value="${redis.port}"/>
        <property name="password" value="${redis.password}"/>
        <property name="usePool" value="${redis.usePool}"/>
        <property name="timeout" value="${redis.timeout}"/>
    </bean>


</beans>
redis.properties :
redis.hostName=192.168.xxx.xxx
redis.port=6379
redis.password=xxxx
redis.usePool=true
redis.timeout=15000
Web.xml :
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">

    <!--SpringSessionRespositoryFilter 這是一個spring-session框架的全局過濾器-->
    <!--org.springframework.web.filter.DelegatingFilterProxy就是我們的SpringSessionRespositoryFilter的jar包-->
    <filter>
        <filter-name>springSessionRepositoryFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSessionRepositoryFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <!--
        (1)因爲涉及到了spring,所以我們需要spring配置文件,所以需要使用 contextLoader監聽器,來監視我們context創建的時刻。
        (2)在context創建的時刻,就將我們的spring配置文件 讀取和加載進我們的內存,然後spring容器中就創建好我們需要的實體對象了。
        (3)使用contextLoaderListener 就是爲了加載我們的 spring配置文件 。
    -->

    <!-- needed for ContextLoaderListener -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <!-- Bootstraps the root web application context before servlet initialization -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

</web-app>

我們的session 通過 Spring-session這個框架存放到了我們的redis中。我們session默認最大空閒時間爲30minute。

這個類因爲有這個註解,所以它有一個定時任務的功能。每個段時間就檢查一下我們的session最後一次訪問時間,看看我們在最後一次訪問之後,有沒有過30分鐘啊,如果30分鐘都沒有訪問過:

就利用 圈1  和  圈3 這兩個 key配合起來 對我們 圈1,2,3 這三個key的刪除。

我們真正的session存放在我們的 圈2裏面,我們圈1 和圈3 是配合起來在session失效的時候刪除這三個key的。

 

Redis中 存放的資源類型:

  1. 動態資源 或 常量:因爲我們的 Redis管理數據是以鍵值對的方式進行管理的。 但是存放的 動態資源和常量我們一般都設置最大生命時間,所以說 在redis中存放的數據 都是不持久的動態資源,雖然可以長久的存放,但是我們人爲的設置了最大生命時間了。
  2. 利用spring-session框架 存放我們的 session對象,並且存放的session對象最大空閒會話時間爲30分鐘。

 

********爲什麼 我們的 spring-session 框架 可以 讓我們的 session對象共享?

由上面 關係圖 我們可以看出 :

(1) redis是在【所有】的tomcat服務器的後面。

(2) redis中 存放了所有的 session對象 。

(3)所有的 tomcat 想獲取 session 都需要去 redis中獲取。

(4)tomcat 從 redis 中 獲取到 session 的過程 不需要我們參與,全權由我們的Spring-session操作完成,spring-session幫我們將session攔截到然後存放進我們的 redis中,然後我們想要獲取的時候,spring-session通過瀏覽器傳遞過來的cookie中的sessionId來自動從redis中幫我們查找對應sessionId的session。

 

總結:

 之所以我們的session共享了,是因爲 我們將所有的session存放進了一個公共的空間了,所有的tomcat都可以隨便的自動的從這個公共空間獲取session了。 原來沒有配置springsession的時候 我們 的session是存放在 tomcat中的容器裏,這個容器是封閉的,是與其他tomcat沒有任何交涉的,所以session不共享。

 

*********我們判斷一個用戶是否登陸的依據是什麼?

我們的用戶登錄之後 我們的tomcat 就會創建一個session對象,這個session對象是和我們的用戶一一對應的,session裏面存放着這個用戶的所有信息。當我們的用戶瀏覽器發送請求後,請求攜帶者cookie中的sessionId到達我們的服務器,然後我們的tomcat服務器從redis中獲取 sessionId對應的session,如果找到了 說明 這個用戶是登陸狀態就顯示登陸,如果沒有找到對應的session對象,說明該用戶沒有登錄,就顯示未登錄狀態。

 

Path是cookie的路徑不相同,產生兩個session:

P2p項目

Show項目

因爲name相同,如果path也相同,那麼他們就是同一個session了,鍵值對嘛,鍵不能重複。

讓不同的項目 共享 session 的 方法 :

指定cookie的存儲方式:

利用下面這個類來進行序列化,修改cookie的存儲格式:

這個類如何序列化,修改cookie存儲格式的:

上面這個類實現了這個接口。

 

配置完成之後:

P2p項目:

Show項目:

這兩個項目都是有同一個瀏覽器用戶發出的請求,所以他們的session應該保持一致,如果不一致,就用這種方法讓session在兩個項目或多個項目中保持共享。

 

域名不相同,項目不相同,怎麼實現session共享:

讓兩個項目的session保存在同一個路徑下:

這兩個項目的根域名相同,所以都存放在根域名下,路徑爲:根域名/  

分別制定:domain 和 path這兩個值。

Web.com是 一級域名。

項目名 爲 二級域名。

包結構:

--Web

            --Com

                      --p2p

                      --show

上面這個地址 是 存放cookie的地址。

Cookie中是用鍵值對來保存 sessionId的。

Name就是key:session 就代表sessionId

Value 裏面就存放着sessionId的值。

Domian + path 就是存放cookie的位置。

 

強行將 cookie放在abc.com這個域名下,這樣違反了cookie安全策略,在寫到瀏覽器的時候 會 被禁止,無法寫到瀏覽器上。Cookie只能保存在當前域名下:  cookie的安全策略不允許寫到別的域名下。

 

這種情況怎麼實現 一個網站登錄其他信任網站也登陸呢?

單點故障:如果只有一個節點一個服務器,如果這個節點服務器發生故障了,整個項目網站服務就不能用了。所以至少佈置兩臺服務器節點,但是兩臺就會出現session不共享的問題,所以我們需要 使用springsession框架實現我們的session共享。

 

 

知識點補充:

Init是初始化方法。以前出現的init方法都是初始化方法。

判斷 日誌級別是否爲 debug級別,如果是debug級別,就打印日誌。

Init初始化方法中的核心代碼:

代理過濾器中重寫這個方法:

GetFilterName()方法底層代碼:

意思是:從配置文件中獲取到<filter-name>之間的值。

 

獲取的是標籤對之間的值:

獲取 相同名字,相同類型過濾器所涉及的方法 :

 

我們 spring容器中獲取到了過濾器的實體bean對象:

<bean id=”springSessionRepositoryFilter” class=”SessionRepositoryFilter的全限定類名”/>

我們調用的是springSessionRepositoryFilter對象中的 doFilter()方法,實質上其實是調用的SessionRepositoryFilter類中的doFilter()方法。

而我們的SessionRepositoryFilter類 中的doFilter()是從我們的OncePerRequestFilter抽象類中繼承過來之後重寫的,OncePerRequestFilter抽象類中的doFilter()是從我們的Filter接口中得到的。

 

調用 dofilter()方法的過程:

這個Dofilter方法中的核心代碼爲:

這個方法在我們的 中被重寫:代碼內容如下:

是這個doFilter()方法的核心代碼,因爲這段代碼和管理session,存儲session有關:

這個內部類 繼承了 父類中的 getSession方法,並且重寫了這個方法。

重寫的是【父類】這裏面的這些方法:

重寫的getSesion方法中的代碼內容:核心代碼紅線(創建一個session對象)

創建session對象的方法的代碼內容如下:

創建session對象的方法中的核心代碼:

我們將session存放進redis中。

添加屬性,如下圖:向Map集合中添加屬性就相當於下圖。redis中的效果圖:

保存session的方法:

上圖doFilter()方法中有下圖這一段代碼,專門是用來存儲session的:

CommitSession方法代碼內容如下:

核心代碼中save方法內容如下:

Save核心代碼 中 saveDelta()方法內容如下:

這個putAll()方法屬於我們的spring-data-redis依賴中的方法,這裏已經到了 spring-data-redis中了:

我們spring-data-redis依賴底層向redis存放數據又要依賴 jedis的jar包:

Jedis幫我們保存 session,jedis涉及的方法如下:

上面的源碼就是這個圖的步驟和知識點。

方法就等於複寫了方法。

 

知識點補充:我們不從配置文件中聲明bean對象,我們用java代碼來實現創建bean對象。

上面 兩種方式 都是相當於在 spring容器中 配置和聲明瞭一個 bean對象。

實際開發中,命名 redis中key的規範:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章