1. 程式人生 > >分散式架構 共享session的常見解決方案

分散式架構 共享session的常見解決方案

在使用分散式架構時,會遇到分散式架構常見的幾個問題:

分散式事務、介面冪等性、分散式鎖和分散式 session。

分散式session

一、什麼是session

瀏覽器在訪問一個web服務的時候,會在瀏覽器中生成一個cookie檔案用於在瀏覽器本地快取資料,這些資料可以根據瀏覽器的設定,持久儲存在瀏覽器本地知道手動清除瀏覽器快取檔案。也可以在結束對一個web服務的會話後(既關閉所有與這個web服務有關的頁面和請求)就清空對這個web服務的cookie資訊。當瀏覽器生成cookie時,每次向web服務端發起請求,都會攜帶一個特殊的jsessionid cookie,根據這個東西,服務端容器(比如tomcat)就會生成一個與之對應的session域,在session域裡存放快取資料。

一般情況下,只要瀏覽器中的cookie還在,與之對應的session就在。但如果cookie沒了,session會根據tomcat配置的生命週期時間,或者web應用的web.xml檔案裡配置的session生命週期時間而結束。我們一般會使用session來快取使用者的登入資訊等,起到一個獲取使用者登入資訊和登入狀態檢查的作用。

單服務系統中,可以直接拿session來用,但是分散式系統中,session狀態怎麼維護?

1.完全不用session

JSON WEB TOKEN (縮寫 JWT)是目前最流行的跨域認證解決方案,我們來介紹一下它的原理和用法:

一、跨域認證問題

1、使用者向伺服器傳送使用者名稱和密碼。

2、伺服器驗證通過後,在當前對話(session)裡面儲存相關資料,比如使用者角色、登入時間等等。

3、伺服器向用戶返回一個 session_id,寫入使用者的 Cookie。

4、使用者隨後的每一次請求,都會通過 Cookie,將 session_id 傳回伺服器。

5、伺服器收到 session_id,找到前期儲存的資料,由此得知使用者的身份。

這種模式的問題在於,擴充套件性(scaling)不好。單機當然沒有問題,如果是伺服器叢集,或者是跨域的服務導向架構,就要求 session 資料共享,每臺伺服器都能夠讀取 session。

舉例來說,A 網站和 B 網站是同一家公司的關聯服務。現在要求,使用者只要在其中一個網站登入,再訪問另一個網站就會自動登入,請問怎麼實現?

一種解決方案是 session 資料持久化,寫入資料庫或別的持久層。各種服務收到請求後,都向持久層請求資料。這種方案的優點是架構清晰,缺點是工程量比較大。另外,持久層萬一掛了,就會單點失敗。

另一種方案是伺服器索性不儲存 session 資料了,所有資料都儲存在客戶端,每次請求都發回伺服器。JWT 就是這種方案的一個代表。

二、JWT 的原理

JWT 的原理是,伺服器認證以後,生成一個 JSON 物件,發回給使用者,就像下面這樣。

{
  "姓名": "張三",
  "角色": "管理員",
  "到期時間": "2018年7月1日0點0分"
}

以後,使用者與服務端通訊的時候,都要發回這個 JSON 物件。伺服器完全只靠這個物件認定使用者身份。為了防止使用者篡改資料,伺服器在生成這個物件的時候,會加上簽名(詳見後文)。伺服器就不儲存任何 session 資料了,也就是說,伺服器變成無狀態了,從而比較容易實現擴充套件。

既在客戶端向服務端發起http請求時,可以在http請求的Header中加入自定義的key:value,自定義的資訊包括了參與加密的資訊,業務資料和簽名信息,然後當服務端收到請求後,根據http請求頭中的這些自定義資訊,以下是 JWT約定的公共引數:

  • iss (issuer):簽發人
  • exp (expiration time):過期時間
  • sub (subject):主題
  • aud (audience):受眾
  • nbf (Not Before):生效時間
  • iat (Issued At):簽發時間
  • jti (JWT ID):編號

通過金鑰等方式,判斷請求是否合法,對合法的請求進行處理,不合法的請求直接無視。不再通過session來判斷使用者合法性和獲取使用者的資訊,實現了跨域系統的訪問。

三、JWT 的幾個特點

(1)JWT 預設是不加密,但也是可以加密的。生成原始 Token 以後,可以用金鑰再加密一次。

(2)JWT 不加密的情況下,不能將祕密資料寫入 JWT。

(3)JWT 不僅可以用於認證,也可以用於交換資訊。有效使用 JWT,可以降低伺服器查詢資料庫的次數。

(4)JWT 的最大缺點是,由於伺服器不儲存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的許可權。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非伺服器部署額外的邏輯。

(5)JWT 本身包含了認證資訊,一旦洩露,任何人都可以獲得該令牌的所有許可權。為了減少盜用,JWT 的有效期應該設定得比較短。對於一些比較重要的許可權,使用時應該再次對使用者進行認證。

(6)為了減少盜用,JWT 不應該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。

2.tomcat + redis

這個其實還挺方便的,就是使用 session 的程式碼,跟以前一樣,還是基於 tomcat 原生的 session 支援即可,然後就是用一個叫做 Tomcat RedisSessionManager 的東西,讓所有我們部署的 tomcat 都將 session 資料儲存到 redis 即可

在 tomcat 的配置檔案中配置:

<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"         host="{redis.host}"         port="{redis.port}"         database="{redis.dbnum}"         maxInactiveInterval="60"/>

然後指定 redis 的 host 和 port 就 ok 了。

<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" /><Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"   sentinelMaster="mymaster"   sentinels="<sentinel1-ip>:26379,<sentinel2-ip>:26379,<sentinel3-ip>:26379"   maxInactiveInterval="60"/>

還可以用上面這種方式基於 redis 哨兵支援的 redis 高可用叢集來儲存 session 資料,都是 ok 的。

3.spring session + redis

上面所說的第二種方式會與 tomcat 容器重耦合,如果我要將 web 容器遷移成 jetty,難道還要重新把 jetty 都配置一遍?

因為上面那種 tomcat + redis 的方式好用,但是會嚴重依賴於web容器,不好將程式碼移植到其他 web 容器上去,尤其是你要是換了技術棧咋整?比如換成了 spring cloud 或者是 spring boot 之類的呢?

所以現在比較好的還是基於 Java 一站式解決方案,也就是 spring。人家 spring 基本上承包了大部分我們需要使用的框架,spirng cloud 做微服務,spring boot 做腳手架,所以用 sping session 是一個很好的選擇。

在 pom.xml中,加入spring session和redis的依賴包,配置:​​​​​​​

<dependency>  <groupId>org.springframework.session</groupId>  <artifactId>spring-session-data-redis</artifactId>  <version>1.2.1.RELEASE</version></dependency>
<dependency>  <groupId>redis.clients</groupId>  <artifactId>jedis</artifactId>  <version>2.8.1</version></dependency>

在 spring的配置檔案中,配置好redis:​​​​​​​

<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">    <property name="maxInactiveIntervalInSeconds" value="600"/></bean>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">    <property name="maxTotal" value="100" />    <property name="maxIdle" value="10" /></bean>
<bean id="jedisConnectionFactory"      class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">    <property name="hostName" value="${redis_hostname}"/>    <property name="port" value="${redis_port}"/>    <property name="password" value="${redis_pwd}" />    <property name="timeout" value="3000"/>    <property name="usePool" value="true"/>    <property name="poolConfig" ref="jedisPoolConfig"/></bean>

在 web.xml 中配置:​​​​​​​

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

示例程式碼:​​​​​​​

@RestController@RequestMapping("/test")public class TestController {
    @RequestMapping("/putIntoSession")    public String putIntoSession(HttpServletRequest request, String username) {        request.getSession().setAttribute("name",  "leo");        return "ok";    }
    @RequestMapping("/getFromSession")    public String getFromSession(HttpServletRequest request, Model model){        String name = request.getSession().getAttribute("name");        return name;    }}

上面的程式碼就是 ok 的,給 sping session 配置基於 redis 來儲存 session 資料,然後配置了一個 spring session 的過濾器,這樣的話,session 相關操作都會交給 spring session 來管了。接著在程式碼中,就用原生的 session 操作,就是直接基於 spring sesion 從 redis 中獲取資料了。

實現分散式的會話有很多種方式,我說的只不過是比較常見的幾種方式,tomcat + redis 早期比較常用,但是會重耦合到 tomcat 中;近些年,通過 spring