1. 程式人生 > >Spring Session 介紹及使用

Spring Session 介紹及使用

spring Session的簡易使用步驟

生成

step 1:後臺業務模組使用spring-Session生成一個session

step 2:後臺業務模組往session裡設定資訊

step 3:將session存到Redis快取中(支援持久化)

step 4:將session id 返回給瀏覽器

step 5:瀏覽器根據cookie方式儲存session id

使用

step 6:瀏覽器取出session id通過HTTP報文帶給後臺

step 7:後臺根據session id從redis裡取出快取的session

step 8:從session中讀取出資訊,進行業務處理。

以下是轉載優秀文章:

長期以來,session管理就是企業級Java中的一部分,以致於我們潛意識就認為它是已經解決的問題,在最近的記憶中,我們沒有看到這個領域有很大的革新。

但是,現代的趨勢是微服務以及可水平擴充套件的原生雲應用(cloud native application),它們會挑戰過去20多年來我們設計和構建session管理器時的前提假設,並且暴露了現代化session管理器的不足。

本文將會闡述最近釋出的Spring Session API如何幫助我們克服眼下session管理方式中的一些不足,在企業級Java中,傳統上都會採用這種舊的方式。我們首先會簡單闡述一下當前session管理中的問題,然後深入介紹Spring Session是如何解決這些問題的。在文章的最後,將會詳細展示Spring Session是如何執行的,以及在專案中怎樣使用它。


編寫可水平擴充套件的原生雲應用。
Spring Session為企業級Java應用的session管理帶來了革新,使得以下的功能更加容易實現:

  • 將session所儲存的狀態解除安裝到特定的外部session儲存中,如Redis或Apache Geode中,它們能夠以獨立於應用伺服器的方式提供高質量的叢集。
  • 當用戶使用WebSocket傳送請求的時候,能夠保持HttpSession處於活躍狀態。
  • 在非Web請求的處理程式碼中,能夠訪問session資料,比如在JMS訊息的處理程式碼中。
  • 支援每個瀏覽器上使用多個session,從而能夠很容易地構建更加豐富的終端使用者體驗。
  • 控制session id如何在客戶端和伺服器之間進行交換,這樣的話就能很容易地編寫Restful API,因為它可以從HTTP 頭資訊中獲取session id,而不必再依賴於cookie。

需要說明的很重要的一點就是,Spring Session的核心專案並不依賴於Spring框架,所以,我們甚至能夠將其應用於不使用Spring框架的專案中。

傳統session管理的問題

傳統的JavaEE session管理會有各種問題,這恰好是Spring Session所要試圖解決的。這些問題在下面以樣例的形式進行了闡述。

構建可水平擴充套件的原生雲應用

在原生的雲應用架構中,會假設應用能夠進行擴充套件,這是通過在Linux容器中執行更多的應用程式例項實現的,這些容器會位於一個大型的虛擬機器池中。例如,我們可以很容易地將一個“.war”檔案部署到位於Cloud Foundry或Heroku的Tomcat中,然後在幾秒鐘的時間內就能擴充套件到100個應用例項,每個例項可以具有1GB RAM。我們還可以配置雲平臺,基於使用者的需求自動增加和減少應用例項的數量。

在很多的應用伺服器中,都會將HTTP session狀態儲存在JVM中,這個JVM與執行應用程式程式碼的JVM是同一個,因為這樣易於實現,並且速度很快。當新的應用伺服器例項加入或離開叢集時,HTTP session會基於現有的應用伺服器例項進行重新平衡。在彈性的雲環境中,我們會擁有上百個應用伺服器例項,並且例項的數量可能在任意時刻增加或減少,這樣的話,我們就會遇到一些問題:

  • 重平衡HTTP session可能會成為效能瓶頸。
  • 為了儲存大量的session,會需要很大的堆空間,這會導致垃圾收集,從而對效能產生負面影響。
  • 雲基礎設施通常會禁止TCP多播(multicast),但是session管理器常常會使用這種機制來發現哪一個應用伺服器例項加入或離開了叢集。

因此,更為高效的辦法是將HTTP session狀態儲存在獨立的資料儲存中,這個儲存位於執行應用程式程式碼的JVM之外。例如,我們可以將100個Tomcat例項配置為使用Redis來儲存session狀態,當Tomcat例項增加或減少的時候,Redis中所儲存的session並不會受到影響。同時,因為Redis是使用C語言編寫的,所以它可以使用上百GB甚至TB級別的RAM,它不會涉及到垃圾收集的問題。

對於像Tomcat這樣的開源伺服器,很容易找到session管理器的替代方案,這些替代方案可以使用外部的資料儲存,如Redis或Memcached。但是,這些配置過程可能會比較複雜,而且每種應用伺服器都有所差別。對於閉源的產品,如WebSphere和Weblogic,尋找它們的session管理器替代方案不僅非常困難,在有些時候,甚至是無法實現的。

Spring Session提供了一種獨立於應用伺服器的方案,這種方案能夠在Servlet規範之內配置可插拔的session資料儲存,不依賴於任何應用伺服器的特定API。這就意味著Spring Session能夠用於實現了servlet規範的所有應用伺服器之中(Tomcat、Jetty、 WebSphere、WebLogic、JBoss等),它能夠非常便利地在所有應用伺服器中以完全相同的方式進行配置。我們還可以選擇任意最適應需求的外部session資料儲存。這使得Spring Session成為一個很理想的遷移工具,幫助我們將傳統的JavaEE應用轉移到雲中,使其成為滿足 12-factor的應用

每個使用者有多個賬號

假設我們在example.com上執行面向公眾的Web應用,在這個應用中有些使用者會建立多個賬號。例如,使用者Jeff Lebowski可能會有兩個賬戶[email protected][email protected]。和其他Java Web應用一樣,我們會使用HttpSession來跟蹤應用的狀態,如當前登入的使用者。所以,當用戶希望從[email protected]切換到[email protected]時,他必須要首先退出,然後再重新登入回來。

藉助Spring Session,為每個使用者配置多個HTTP session會非常容易,這樣使用者在[email protected][email protected]之間切換的時候,就不需要退出和重新登入了。

多級別的安全預覽

假設我們正在構建的Web應用有一個複雜、自定義的許可權功能,其中應用的UI會基於使用者所授予的角色和許可權實現自適應。

例如,假設應用有四個安全級別:public、confidential、secret和top secret。當用戶登入應用之後,系統會判斷使用者所具有的最高安全級別並且只會顯示該級別和該級別之下的資料。所以,具有public許可權的使用者只能看到public級別的文件,具有secret許可權的使用者能夠看到public、confidential和secret級別的文件,諸如此類。為了保證使用者介面更加友好,應用程式應該允許使用者預覽在較低的安全級別條件下頁面是什麼樣子的。例如,top secret許可權的使用者能夠將應用從top secret模式切換到secret模式,這樣就能站在具有secret許可權使用者的視角上,檢視應用是什麼樣子的。

典型的Web應用會將當前使用者的標識及其角色儲存在HTTP session中,但因為在Web應用中,每個登入的使用者只能有一個session,因此除了使用者退出並重新登入進來,我們並沒有辦法在角色之間進行切換,除非我們為每個使用者自行實現多個session的功能。

藉助Spring Session,可以很容易地為每個登入使用者建立多個session,這些session之間是完全獨立的,因此實現上述的預覽功能是非常容易的。例如,當前使用者以top secret角色進行了登入,那麼應用可以建立一個新的session,這個session的最高安全形色是secret而不是top secret,這樣的話,使用者就可以在secret模式預覽應用了。

當使用Web Socket的時候保持登入狀態

假設使用者登入了example.com上的Web應用,那麼他們可以使用HTML5的chat客戶端實現聊天的功能,這個客戶端構建在websocket之上。按照servlet規範,通過websocket傳入的請求並不能保持HTTP session處於活躍狀態,所以當用戶在聊天的過程中,HTTP session的倒數計時器會在不斷地流逝。即便站在使用者的立場上,他們一直在使用應用程式,HTTP session最終也可能會出現過期。當HTTP session過期時,websocket連線將會關閉。

藉助Spring Session,對於系統中的使用者,我們能夠很容易地實現websocket請求和常規的HTTP請求都能保持HTTP session處於活躍狀態。

非Web請求訪問Session資料

假設我們的應用提供了兩種訪問方式:一種使用基於HTTP的REST API,而另一種使用基於RabbitMQ的AMQP訊息。執行訊息處理程式碼的執行緒將無法訪問應用伺服器的HttpSession,所以我們必須要以一種自定義的方案來獲取HTTP session中的資料,這要通過自定義的機制來實現。

通過使用Spring Session,只要我們能夠知道session的id,就可以在應用的任意執行緒中訪問Spring Session。因此,Spring Session具備比Servlet HTTP session管理器更為豐富的API,只要知道了session id,我們就能獲取任意特定的session。例如,在一個傳入的訊息中可能會包含使用者id的header資訊,藉助它,我們就可以直接獲取session了。

Spring Session是如何執行的

我們已經討論了在傳統的應用伺服器中,HTTP session管理存在不足的各種場景,接下來看一下Spring Session是如何解決這些問題的。

Spring Session的架構

當實現session管理器的時候,有兩個必須要解決的核心問題。首先,如何建立叢集環境下高可用的session,要求能夠可靠並高效地儲存資料。其次,不管請求是HTTP、WebSocket、AMQP還是其他的協議,對於傳入的請求該如何確定該用哪個session例項。實質上,關鍵問題在於:在發起請求的協議上,session id該如何進行傳輸?

Spring Session認為第一個問題,也就是在高可用可擴充套件的叢集中儲存資料已經通過各種資料儲存方案得到了解決,如Redis、GemFire以及Apache Geode等等,因此,Spring Session定義了一組標準的介面,可以通過實現這些介面間接訪問底層的資料儲存。Spring Session定義瞭如下核心介面:Session、ExpiringSession以及SessionRepository,針對不同的資料儲存,它們需要分別實現。

  • org.springframework.session.Session介面定義了session的基本功能,如設定和移除屬性。這個介面並不關心底層技術,因此能夠比servlet HttpSession適用於更為廣泛的場景中。
  • org.springframework.session.ExpiringSession擴充套件了Session介面,它提供了判斷session是否過期的屬性。RedisSession是這個介面的一個樣例實現。
  • org.springframework.session.SessionRepository定義了建立、儲存、刪除以及檢索session的方法。將Session例項真正儲存到資料儲存的邏輯是在這個介面的實現中編碼完成的。例如,RedisOperationsSessionRepository就是這個介面的一個實現,它會在Redis中建立、儲存和刪除session。

Spring Session認為將請求與特定的session例項關聯起來的問題是與協議相關的,因為在請求/響應週期中,客戶端和伺服器之間需要協商同意一種傳遞session id的方式。例如,如果請求是通過HTTP傳遞進來的,那麼session可以通過HTTP cookie或HTTP Header資訊與請求進行關聯。如果使用HTTPS的話,那麼可以藉助SSL session id實現請求與session的關聯。如果使用JMS的話,那麼JMS的Header資訊能夠用來儲存請求和響應之間的session id。

對於HTTP協議來說,Spring Session定義了HttpSessionStrategy介面以及兩個預設實現,即CookieHttpSessionStrategyHeaderHttpSessionStrategy,其中前者使用HTTP cookie將請求與session id關聯,而後者使用HTTP header將請求與session關聯。

如下的章節詳細闡述了Spring Session使用HTTP協議的細節。

在撰寫本文的時候,在當前的Spring Session 1.0.2 GA釋出版本中,包含了Spring Session使用Redis的實現,以及基於Map的實現,這個實現支援任意的分散式Map,如Hazelcast。讓Spring Session支援某種資料儲存是相當容易的,現在有支援各種資料儲存的社群實現。

Spring Session對HTTP的支援

Spring Session對HTTP的支援是通過標準的servlet filter來實現的,這個filter必須要配置為攔截所有的web應用請求,並且它應該是filter鏈中的第一個filter。Spring Session filter會確保隨後呼叫javax.servlet.http.HttpServletRequestgetSession()方法時,都會返回Spring Session的HttpSession例項,而不是應用伺服器預設的HttpSession。

如果要理解它的話,最簡單的方式就是檢視Spring Session實際所使用的原始碼。首先,我們瞭解一下標準servlet擴充套件點的一些背景知識,在實現Spring Session的時候會使用這些知識。

在2001年,Servlet 2.3規範引入了ServletRequestWrapper它的javadoc文件這樣寫道ServletRequestWrapper“提供了ServletRequest介面的便利實現,開發人員如果希望將請求適配到Servlet的話,可以編寫它的子類。這個類實現了包裝(Wrapper)或者說是裝飾(Decorator)模式。對方法的呼叫預設會通過包裝的請求物件來執行”。如下的程式碼樣例抽取自Tomcat,展現了ServletRequestWrapper是如何實現的。

public class ServletRequestWrapper implements ServletRequest {

    private ServletRequest request;

    /**
     * 建立ServletRequest介面卡,它包裝了給定的請求物件。
     * @throws java.lang.IllegalArgumentException if the request is null
     */
    public ServletRequestWrapper(ServletRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Request cannot be null");   
        }
        this.request = request;
    }

    public ServletRequest getRequest() {
        return this.request;
    }
    
    public Object getAttribute(String name) {
        return this.request.getAttribute(name);
    }

    // 為了保證可讀性,其他的方法刪減掉了 
}

Servlet 2.3規範還定義了HttpServletRequestWrapper,它是ServletRequestWrapper的子類,能夠快速提供HttpServletRequest的自定義實現,如下的程式碼是從Tomcat抽取出來的,展現了

HttpServletRequesWrapper類是如何執行的。

public class HttpServletRequestWrapper extends ServletRequestWrapper 
    implements HttpServletRequest {

    public HttpServletRequestWrapper(HttpServletRequest request) {
	    super(request);
    }
    
    private HttpServletRequest _getHttpServletRequest() {
 	   return (HttpServletRequest) super.getRequest();
    }
  
    public HttpSession getSession(boolean create) {
     return this._getHttpServletRequest().getSession(create);
    }
   
    public HttpSession getSession() {
      return this._getHttpServletRequest().getSession();
    }
  // 為了保證可讀性,其他的方法刪減掉了  
}

所以,藉助這些包裝類就能編寫程式碼來擴充套件HttpServletRequest,過載返回HttpSession的方法,讓它返回由外部儲存所提供的實現。如下的程式碼是從Spring Session專案中提取出來的,但是我將原來的註釋替換為我自己的註釋,用來在本文中解釋程式碼,所以在閱讀下面的程式碼片段時,請留意註釋。

/*
 * 注意,Spring Session專案定義了擴充套件自
 * 標準HttpServletRequestWrapper的類,用來過載
 * HttpServletRequest中與session相關的方法。
 */
private final class SessionRepositoryRequestWrapper
   extends HttpServletRequestWrapper {

   private HttpSessionWrapper currentSession;
   private Boolean requestedSessionIdValid;
   private boolean requestedSessionInvalidated;
   private final HttpServletResponse response;
   private final ServletContext servletContext;

   /*
   * 注意,這個構造器非常簡單,它接受稍後會用到的引數,
   * 並且委託給它所擴充套件的HttpServletRequestWrapper
   */
   private SessionRepositoryRequestWrapper(
      HttpServletRequest request,
      HttpServletResponse response,
      ServletContext servletContext) {
     super(request);
     this.response = response;
     this.servletContext = servletContext;
   }

   /*
   * 在這裡,Spring Session專案不再將呼叫委託給
   * 應用伺服器,而是實現自己的邏輯,
   * 返回由外部資料儲存作為支撐的HttpSession例項。
   *
   * 基本的實現是,先檢查是不是已經有session了。如果有的話,
   * 就將其返回,否則的話,它會檢查當前的請求中是否有session id。
   * 如果有的話,將會根據這個session id,從它的SessionRepository中載入session。
   * 如果session repository中沒有session,或者在當前請求中,
   * 沒有當前session id與請求關聯的話,
   * 那麼它會建立一個新的session,並將其持久化到session repository中。
   */
   @Override
   public HttpSession getSession(boolean create) {
     if(currentSession != null) {
       return currentSession;
     }
     String requestedSessionId = getRequestedSessionId();
     if(requestedSessionId != null) {
       S session = sessionRepository.getSession(requestedSessionId);
       if(session != null) {
         this.requestedSessionIdValid = true;
         currentSession =