深入理解 Session 與 Cookie

摘要 Session 與 Cookie 不管是對 Java Web 的初學者還是熟練使用者來說都是一個令人頭疼的問題。在初入職場時恐怕很多程序員在面試的時候都被問到過這個問題。其實這個問題回答起來既簡單又複雜,簡單是因爲它們本身只是 HTTP 協議中的一個配置項,在 Servlet 規範中也只是對應到一個類而已;說它複雜原因在於當我們的系統大到需要用到很多 Cookie 的時候,我們不得不考慮 HTTP 協議對 Cookie 數量和大小的限制,那麼如何才能解決這個瓶頸呢? Session 也會有同樣的問題,當我們的一個應用系統有幾百臺服務器的時候如何解決 Session 在多臺服務器之間共享?...

Session 與 Cookie 的作用都是爲了保持訪問用戶與後端服務器的交互狀態。它們有各自的優點,也有各自的缺陷,然而具有諷刺意味的是它們的優點和它們的使用場景又是矛盾的。例如,使用 Cookie 來傳遞信息時,隨着 Cookie 個數的增多和訪問量的增加,它佔用的網絡帶寬也很大,試想假如 Cookie 佔用 200 個字節,如果一天的 PV 有幾億,它要佔用多少帶寬?所以有大訪問量的時候希望用 Session,但是 Session 的致命弱點是不容易在多臺服務器之間共享,所以這也限制了 Session 的使用。

10.1 理解 Cookie

Cookie 的作用我想大家都知道,通俗地說就是當一個用戶通過 HTTP 協議訪問一個服務器的時候,這個服務器會將一些 Key/Value 鍵值對返回給客戶端瀏覽器,並給這些數據加上一些限制條件,在條件符合時這個用戶下次訪問這個服務器的時候,數據又被完整地帶回給服務器。

這個作用就像您去超市購物時,第一次給您辦張購物卡,這個購物卡里存放了一些您的個人信息,下次您再來這個連鎖超市時,超市會識別您的購物卡,下次直接購物就好了。

當初 W3C 在設計 Cookie 時實際上考慮的是爲了記錄用戶在一段時間內訪問 Web 應用的行爲路徑。由於 HTTP 協議是一種無狀態協議,當用戶的一次訪問請求結束後,後端服務器就無法知道下一次來訪問的還是不是上次訪問的用戶,在設計應用程序時,我們很容易想到兩次訪問是同一人訪問與不同的兩個人訪問對程序設計和性能來說有很大的不同。例如,在一個很短的時間內,如果與用戶相關的數據被頻繁訪問,可以針對這個數據做緩存,這樣可以大大提高數據的訪問性能。Cookie 的作用正是在此,由於是同一個客戶端發出的請求,每次發出的請求都會帶有第一次訪問時服務端設置的信息,這樣服務端就可以根據 Cookie 值來劃分訪問的用戶了。

10.1.1 Cookie 屬性項

當前 Cookie 有兩個版本:Version 0 和 Version 1。通過它們有兩種設置響應頭的標識,分別是 “Set-Cookie”和“Set-Cookie2”。這兩個版本的屬性項有些不同,表 10-1 和表 10-2 是兩個版本的屬性介紹。

表 10-1.Version 0 屬性項介紹
屬性項 屬性項介紹
NAME=VALUE 鍵值對,可以設置要保存的 Key/Value,注意這裏的 NAME 不能和其他屬性項的名字一樣
Expires 過期時間,在設置的某個時間點後該 Cookie 就會失效,如 expires=Wednesday, 09-Nov-99 23:12:40 GMT
Domain 生成該 Cookie 的域名,如 domain="xulingbo.net"
Path 該 Cookie 是在當前的哪個路徑下生成的,如 path=/wp-admin/
Secure 如果設置了這個屬性,那麼只會在 SSH 連接時纔會回傳該 Cookie
表 10-2.Version 1 屬性項介紹
屬 性 項 屬性項介紹
NAME=VALUE 與 Version 0 相同
Version 通過 Set-Cookie2 設置的響應頭創建必須符合 RFC2965 規範,如果通過 Set-Cookie 響應頭設置,默認值爲 0,如果要設置爲 1,則該 Cookie 要遵循 RFC 2109 規範
Comment 註釋項,用戶說明該 Cookie 有何用途
CommentURL 服務器爲此  Cookie 提供的 URI 註釋
Discard 是否在會話結束後丟棄該 Cookie 項,默認爲 fasle
Domain 類似於 Version 0
Max-Age 最大失效時間,與 Version 0 不同的是這裏設置的是在多少秒後失效
Path 類似於 Version 0
Port 該 Cookie 在什麼端口下可以回傳服務端,如果有多個端口,以逗號隔開,如 Port="80,81,8080"
Secure 類似於 Version 0

以上兩個版本的 Cookie 中設置的 Header 頭的標識符是不同的,我們常用的是 Set-Cookie:userName=“junshan”; Domain=“xulingbo.net”,這是 Version 0 的形式。針對 Set-Cookie2 是這樣設置的:Set-Cookie2:userName=“junshan”; Domain=“xulingbo.net”; Max-Age=1000。但是在 Java Web 的 Servlet 規範中並不支持 Set-Cookie2 響應頭,在實際應用中 Set-Cookie2 的一些屬性項卻可以設置在 Set-Cookie 中,如這樣設置:Set-Cookie:userName=“junshan”; Version=“1”;Domain=“xulingbo.net”;Max-Age=1000。

10.1.2 Cookie 如何工作

當我們用如下方式創建 Cookie 時:

 String getCookie(Cookie[] cookies, String key) { 
        if (cookies != null) { 
            for (Cookie cookie : cookies) { 
                if (cookie.getName().equals(key)) { 
                    return cookie.getValue(); 
                } 
            } 
        } 
        return null; 
    } 

    @Override 
    public void doGet(HttpServletRequest request, 
                      HttpServletResponse response) 
            throws IOException, ServletException { 
        Cookie[] cookies = request.getCookies(); 
        String userName = getCookie(cookies, "userName"); 
        String userAge = getCookie(cookies, "userAge"); 
        if (userName == null) { 
            response.addCookie(new Cookie("userName", "junshan")); 
        } 
        if (userAge == null) { 
            response.addCookie(new Cookie("userAge", "28")); 
        } 
        response.getHeaders("Set-Cookie"); 
 }

Cookie 是如何加到 HTTP 的 Header 中的?當我們用 Servlet 3.0 規範來創建一個 Cookie 對象時,該 Cookie 既支持 Version 0 又支持 Version 1,如果您設置了 Version 1 中的配置項,即使您沒有設置版本號,Tomcat 在最後構建 HTTP 響應頭時也會自動將 Version 的版本設置爲 1。下面看一下 Tomcat 是如何調用 addCookie 方法,圖 10-1 是 Tomcat 創建 Set-Cookie 響應頭的時序圖。

圖 10-1.Tomcat 創建 Set-Cookie 響應頭的時序圖

從圖 10-1 中可以看出,真正構建 Cookie 是在 org.apache.catalina.connector. Response 類中完成的,調用 generateCookieString 方法將 Cookie 對象構造成一個字符串,構造的字符串的格式如 userName=“junshan”;Version=“1”; Domain=“xulingbo.net”; Max-Age=1000。然後將這個字符串命名爲 Set-Cookie 添加到 MimeHeaders 中。

在這裏有幾點需要注意:

  • 創建的 Cookie 的 NAME 不能和 Set-Cookie 或者 Set-Cookie2 的屬性項值一樣,如果一樣會拋 IllegalArgumentException 異常。

  • 創建 Cookie 的 NAME 和 VALUE 的值不能設置成非 ASSIC 字符,如果要使用中文,可以通過 URLEncoder 將其編碼,否則將會拋 IllegalArgumentException 異常。

  • 當 NAME 和 VALUE 的值出現一些 TOKEN 字符(如“\”、“,”等)時,構建返回頭會將該 Cookie 的 Version 自動設置爲 1。

  • 當該 Cookie 的屬性項中出現 Version 爲 1 的屬性項時,構建 HTTP 響應頭同樣會將 Version 設置爲 1。

不知道您有沒有注意一個問題,就是當我們通過 response.addCookie 創建多個 Cookie 時,這些 Cookie 最終是在一個 Header 項中還是以獨立的 Header 存在的,通俗地說也就是我們每次創建 Cookie 時是否都是創建一個以 NAME 爲 Set-Cookie 的 MimeHeaders ?答案是肯定的。從上面的時序圖中可以看出每次調用 addCookie 的時候,最終都會創建一個 Header,但是我們還不知道最終在請求返回時構造 HTTP 響應頭是否將相同 Header 標識的 Set-Cookie 值進行合併。

我們找到 Tomcat 最終構造 HTTP 響應頭的代碼,這段代碼位於 org.apache.coyote.http11. Http11Processor 類的 prepareResponse 方法中,如下所示:

 int size = headers.size(); 
 for (int i = 0; i < size; i++) { 
    outputBuffer.sendHeader(headers.getName(i), headers.getValue(i)); 
 }

這段代碼清楚地表示,在構建 HTTP 返回字節流時是將 Header 中所有的項順序地寫出,而沒有進行任何修改。所以可以想象瀏覽器在接收 HTTP 協議返回的數據時是分別解析每一個 Header 項的。

另外,目前很多工具都可以觀察甚至可以修改瀏覽器中的 Cookie 數據。例如,在 Firefox 中可以通過 HttpFox 插件來查看返回的 Cookie 數據,如圖 10-2 所示。

圖 10-2.HttpFox 插件展示的 Header 數據

在 cookie 項中可以詳細查看 Cookie 屬性項,如圖 10-3 所示。

圖 10-3.HttpFox 插件展示的 Cookie 數據

前面主要介紹了服務端如何創建 Cookie,下面看一下如何從客戶端獲取 Cookie。

當我們請求某個 URL 路徑時,瀏覽器會根據這個 URL 路徑將符合條件的 Cookie 放在 Request 請求頭中傳回給服務端,服務端通過 request.getCookies() 來取得所有 Cookie。

10.1.3 使用 Cookie 的限制

Cookie 是 HTTP 協議頭中的一個字段,雖然 HTTP 協議本身對這個字段並沒有多少限制,但是 Cookie 最終還是存儲在瀏覽器裏,所以不同的瀏覽器對 Cookie 的存儲都有一些限制,表 10-3 是一些通常的瀏覽器對 Cookie 的大小和數量的限制。

表 10-3.瀏覽器對 Cookie 的大小和數量的限制
瀏覽器版本 Cookie 數限制 Cookie 總大小限制
IE6 20 個 / 每個域名 4095 個字節
IE7 50 個 / 每個域名 4095 個字節

續表

瀏覽器版本 Cookie 數限制 Cookie 總大小限制
IE8 50 個 / 每個域名 4095 個字節
IE9 50 個 / 每個域名 4095 個字節
Chrome 50 個 / 每個域名 大於 80000
FireFox 50 個 / 每個域名 4097 個字

10.2 理解 Session

前面已經介紹了 Cookie 可以讓服務端程序跟蹤每個客戶端的訪問,但是每次客戶端的訪問都必須傳回這些 Cookie,如果 Cookie 很多,這無形地增加了客戶端與服務端的數據傳輸量,而 Session 的出現正是爲了解決這個問題。

同一個客戶端每次和服務端交互時,不需要每次都傳回所有的 Cookie 值,而是隻要傳回一個 ID,這個 ID 是客戶端第一次訪問服務器的時候生成的,而且每個客戶端是唯一的。這樣每個客戶端就有了一個唯一的 ID,客戶端只要傳回這個 ID 就行了,這個 ID 通常是 NANE 爲 JSESIONID 的一個 Cookie。

10.2.1 Session 與 Cookie

下面詳細講一下 Session 如何基於 Cookie 來工作。實際上有三種方式能可以讓 Session 正常工作:

  • 基於 URL Path Parameter,默認支持。

  • 基於 Cookie,如果沒有修改 Context 容器的 cookies 標識,默認也是支持的。

  • 基於 SSL,默認不支持,只有 connector.getAttribute("SSLEnabled") 爲 TRUE 時才支持。

第一種情況下,當瀏覽器不支持 Cookie 功能時,瀏覽器會將用戶的 SessionCookieName 重寫到用戶請求的 URL 參數中,它的傳遞格式如 /path/Servlet;name=value;name2=value2? Name3=value3,其中“Servlet;”後面的 K-V 就是要傳遞的 Path Parameters,服務器會從這個 Path Parameters 中拿到用戶配置的 SessionCookieName。關於這個 SessionCookieName,如果在 web.xml 中配置 session-config 配置項,其 cookie-config 下的 name 屬性就是這個 SessionCookieName 值。如果沒有配置了 session-config 配置項,默認的 SessionCookieName 就是大家熟悉的“JSESSIONID”。需要說明的一點是,與 Session 關聯的 Cookie 與其他 Cookie 沒有什麼不同。接着 Request 根據這個 SessionCookieName 到 Parameters 中拿到 Session ID 並設置到 request.setRequestedSessionId 中。

請注意,如果客戶端也支持 Cookie,Tomcat 仍然會解析 Cookie 中的 Session ID,並會覆蓋 URL 中的 Session ID。

如果是第三種情況,將會根據 javax.servlet.request.ssl_session 屬性值設置 Session ID。

10.2.2 Session 如何工作

有了 Session ID 服務端就可以創建 HttpSession 對象了,第一次觸發通過 request.getSession() 方法。如果當前的 Session ID 還沒有對應的 HttpSession 對象,那麼就創建一個新的,並將這個對象加到 org.apache.catalina. Manager 的 sessions 容器中保存。Manager 類將管理所有 Session 的生命週期,Session 過期將被回收,服務器關閉,Session 將被序列化到磁盤等。只要這個 HttpSession 對象存在,用戶就可以根據 Session ID 來獲取這個對象,也就達到了狀態的保持。

Session 相關類圖如圖 10-4 所示。

圖 10-4.Session 相關類圖

從圖 10-4 中可以看出,從 request.getSession 中獲取的 HttpSession 對象實際上是 StandardSession 對象的門面對象,這與前面的 Request 和 Servlet 是一樣的原理。圖 10-5 是 Session 工作的時序圖。

圖 10-5.Session 工作的時序圖(查看大圖

從時序圖中可以看出,從 Request 中獲取的 Session 對象保存在 org.apache. catalina.Manager 類中,它的實現類是 org.apache.catalina.session.StandardManager,通過 requestedSessionId 從 StandardManager 的 sessions 集合中取出 StandardSession 對象。由於一個 requestedSessionId 對應一個訪問的客戶端,所以一個客戶端,也就對應一個 StandardSession 對象,這個對象正是保存我們創建的 Session 值的。下面我們看一下 StandardManager 這個類是如何管理 StandardSession 的生命週期的。

圖 10-6.StandardManager 與 StandardSession 的類關係圖

StandardManager 類負責 Servlet 容器中所有的 StandardSession 對象的生命週期管理。當 Servlet 容器重啓或關閉時 StandardManager 負責持久化沒有過期的 StandardSession 對象,它會將所有的 StandardSession 對象持久化到一個以“SESSIONS.ser”爲文件名的文件中。到 Servlet 容器重啓時,也就是 StandardManager 初始化時,會重新讀取這個文件解析出所有 Session 對象,重新保存在 StandardManager 的 sessions 集合中。Session 恢復狀態圖如圖 10-7 所示。

圖 10-7.Session 恢復狀態圖

當 Servlet 容器關閉時 StandardManager 類會調用 unload 方法將 sessions 集合中的 StandardSession 對象寫到“SESSIONS.ser”文件中,然後在啓動時再按照上面的狀態圖重新恢復,注意要持久化保存 Servlet 容器中的 Session 對象,必須調用 Servlet 容器的 stop 和 start 命令,而不能直接結束(kill)Servlet 容器的進程,因爲直接結束進程,Servlet 容器沒有機會調用 unload 方法來持久化這些 Session 對象。

另外,StandardManager 中的 sessions 集合中的 StandardSession 對象並不是永遠保存的,否則 Servlet 容器的內存將很容易被消耗盡,所以必須給每個 Session 對象定義一個有效時間,超過這個時間 Session 對象將被清除。在 Tomcat 中這個有效時間是 60(maxInactiveInterval 屬性控制)秒,超過 60 秒該 Session 將會過期。檢查每個 Session 是否失效是在 Tomcat 的一個後臺線程中完成的(backgroundProcess() 方法中)。過期 Session 狀態圖如圖 10-8 所示。

圖 10-8.過期 Session 狀態圖

除了後臺進程檢查 Session 是否失效外,當調用 request.getSession() 時也會檢查該 Session 是否過期。值得注意的是,request.getSession() 方法調用的 StandardSession 永遠都會存在,即使與這個客戶端關聯的 Session 對象已經過期。如果過期,又會重新創建一個全新的 StandardSession 對象,但是以前設置的 Session 值將會丟失。如果您取到了 Session 對象但是通過 session.getAttribute 取不到前面設置的 Session 值,請不要奇怪,因爲很可能它已經失效了,請檢查一下 <Manager pathname="" maxInactiveInterval="60" /> 中 maxInactiveInterval 配置項的值,如果不想讓 Session 過期可以設置爲 -1。但是您要仔細評估一下,網站的訪問量和設置的 Session 的大小,防止將您的 Servlet 容器內存撐爆。如果不想自動創建 Session 對象,也可以通過 request.getSession(boolean create) 方法來判斷該客戶端關聯的 Session 對象是否存在。

10.3 Cookie 安全問題

雖然 Cookie 和 Session 都可以跟蹤客戶端的訪問記錄,但是它們的工作方式顯然是不同的,Cookie 通過把所有要保存的數據通過 HTTP 協議的頭部從客戶端傳遞到服務端,又從服務端再傳回到客戶端,所有的數據都存儲在客戶端的瀏覽器裏,所以這些 Cookie 數據可以被訪問到,就像我們前面通過 Firefox 的插件 HttpFox 可以看到所有的 Cookie 值。不僅可以查看 Cookie,甚至可以通過 Firecookie 插件添加、修改 Cookie,所以 Cookie 的安全性受到了很大的挑戰。

相比較而言 Session 的安全性要高很多,因爲 Session 是將數據保存在服務端,只是通過 Cookie 傳遞一個 SessionID 而已,所以 Session 更適合存儲用戶隱私和重要的數據。

10.4 分佈式 Session 框架

從前面的分析可知,Session 和 Cookie 各自有優點和缺點。在大型互聯網系統中,單獨使用 Cookie 和 Session 都是不可行的,原因很簡單。因爲如果使用 Cookie,可以很好地解決應用的分佈式部署問題,大型互聯網應用系統一個應用有上百臺機器,而且有很多不同的應用系統協同工作,由於 Cookie 是將值存儲在客戶端的瀏覽器裏,用戶每次訪問都會將最新的值帶回給處理該請求的服務器,所以也就解決了同一個用戶的請求可能不在同一臺服務器處理而導致的 Cookie 不一致的問題。

10.4.1 存在哪些問題

這種“誰家的孩子誰抱走”的處理方式的確是大型互聯網的一個比較簡單但是的確可以解決問題的處理方式,但是這種處理方式也會帶來了很多其他問題,如:

  • 客戶端 Cookie 存儲限制。隨着應用系統的增多 Cookie 數量也快速增加,但瀏覽器對於用戶 Cookie 的存儲是有限制的。例如,IE7 之前的 IE 瀏覽器,Cookie 個數的限制是 20 個,後續的版本,包括 Firefox 等,Cookie 個數的限制都是 50 個。總大小不超過 4KB,超過限制就會出現丟棄 Cookie 的現象發生,這會嚴重影響應用系統的正常使用。

  • Cookie 管理的混亂。在大型互聯網應用系統中,如果每個應用系統都自己管理每個應用使用的 Cookie,將會導致混亂,由於通常應用系統都在同一個域名下,Cookie 又有上面一條提到的限制,所以沒有統一管理很容易出現 Cookie 超出限制的情況。

  • 安全令人擔憂。雖然可以通過設置 HttpOnly 屬性防止一些私密 Cookie 被客戶端訪問,但是仍然不能保證 Cookie 無法被篡改。爲了保證 Cookie 的私密性通常會對 Cookie 進行加密,但是維護這個加密 Key 也是一件麻煩的事情,無法保證定期來更新加密 Key 也是帶來安全性問題的一個重要因素。

當以上問題不能再容忍下去的時候,就不得不想其他辦法處理了。

10.4.2 可以解決哪些問題

既然 Cookie 有以上這些問題,Session 也有它的好處,爲何不結合使用 Session 和 Cookie 呢?下面是分佈式 Session 框架可以解決的問題:

  • Session 配置的統一管理。

  • Cookie 使用的監控和統一規範管理。

  • Session 存儲的多元化。

  • Session 配置的動態修改。

  • Session 加密 key 的定期修改。

  • 充分的容災機制,保持框架的使用穩定性。

  • Session 各種存儲的監控和報警支持。

  • Session 框架的可擴展性,兼容更多的 session 機制如 wapSession。

  • 跨域名 Session 與 Cookie 如何共享,現在同一個網站可能存在多個域名,如何將 Session 和 Cookie 在不同的域名之間共享是一個具有挑戰性的問題。

10.4.3 總體實現思路

分佈式 Session 框架的架構圖如圖 10-9 所示。

爲了達成上面所說的幾點目標,我們需要一個服務訂閱服務器,在應用啓動時可以從這個訂閱服務器訂閱這個應用需要的可寫 Session 項和可寫 Cookie 項,這些配置的 Session 和 Cookie 可以限制這個應用能夠使用哪些 Session 和 Cookie,甚至可以控制 Session 和 Cookie 可讀或者可寫。這樣可以精確地控制哪些應用可以操作哪些 Session 和 Cookie,可以有效控制 Session 的安全性和 Cookie 的數量。

圖 10-9.Session 框架的架構圖

如 Session 的配置項可以爲如下形式:

 <session> 
       <key>sessionID</key> 
      <ookiekey>sessionID</ookiekey > 
       <lifeCycle>9000</lifeCycle> 
      <base64>true</base64> 
 </session >

Cookie 的配置可以爲如下形式:

 <cookie> 
       <key>cookie</key> 
       <lifeCycle></lifeCycle> 
       <type>1</type> 
       <path>/wp</path> 
      <domain>xulingbo.net</ domain> 
       <decrypt>false</decrypt> 
      <httpOnly>false</ httpOnly > 
 </cookie>

統一通過訂閱服務器推送配置可以有效地集中管理資源,所以可以省去每個應用都來配置 Cookie,簡化 Cookie 的管理。如果應用要使用一個新增 Cookie,可以通過一個統一的平臺來申請,申請通過纔將這個配置項增加到訂閱服務器。如果是一個所有應用都要使用的全局 Cookie,那麼只需將這個 Cookie 通過訂閱服務器統一推送過去就行了,省去了要在每個應用中手動增加 Cookie 的配置。

關於這個訂閱服務器現在有很多開源的配置服務器,如 Zookeeper 集羣管理服務器,可以統一管理所有服務器的配置文件。

由於應用是一個集羣,所以不可能將創建的 Session 都保存在每臺應用服務器的內存中,因爲如果每臺服務器有幾十萬的訪問用戶,服務器的內存肯定不夠用,即使內存夠用,這些 Session 也無法同步到這個應用的所有服務器中。所以要共享這些 Session 必須將它們存儲在一個分佈式緩存中,可以隨時寫入和讀取,而且性能要很好才能滿足要求。當前能滿足這個要求的系統有很多,如 MemCache 或者淘寶的開源分佈式緩存系統 Tair 都是很好的選擇。

解決了配置和存儲問題,下面看一下如何存取 Session 和 Cookie。

既然是一個分佈式 Session 的處理框架,必然會重新實現 HttpSession 的操作接口,使得應用操作 Session 的對象都是我們實現的 InnerHttpSession 對象,這個操作必須在進入應用之前完成,所以可以配置一個 filter 攔截用戶的請求。

先看一下如何封裝 HttpSession 對象和攔截請求,圖 10-10 是時序圖。

我們可以在應用的 web.xml 中配置一個 SessionFilter,用於在請求到達 MVC 框架之前封裝 HttpServletRequest 和 HttpServletResponse 對象,並創建我們自己的 InnerHttpSession 對象,把它設置到 request 和 response 對象中。這樣應用系統通過 request.getHttpSession() 返回的就是我們創建的 InnerHttpSession 對象了,我們可以攔截 response 的 addCookies 設置的 Cookie。

在時序圖中,應用創建的所有 Session 對象都會保存在 InnerHttpSession 對象中,當用戶的這次訪問請求完成時,Session 框架將會把這個 InnerHttpSession 的所有內容再更新到分佈式緩存中,以便於這個用戶通過其他服務器再次訪問這個應用系統。另外,爲了保證一些應用對 Session 穩定性的特殊要求可以將一些非常關鍵的 Session 再存儲到 Cookie 中,如當分佈式緩存存在問題時,可以將部分 Session 存儲到 Cookie 中,這樣即使分佈式緩存出現問題也不會影響關鍵業務的正常運行。

圖 10-10.HttpSession 攔截請求時序圖(查看大圖

還有一個非常重要的問題就是如何處理跨域名來共享 Cookie 的問題。我們知道 Cookie 是有域名限制的,也就是一個域名下的 Cookie 不能被另一個域名訪問,所以如果在一個域名下已經登錄成功,如何訪問到另外一個域名的應用且保證登錄狀態仍然有效,這個問題大型網站應該經常會遇到。如何解決這個問題呢?下面介紹一種處理方式,如圖 10-11 所示。

圖 10-11.跨域名同步 session(查看大圖

從圖中可以看出,要實現 Session 同步,需要另外一個跳轉應用,這個應用可以被一個或者多個域名訪問,它的主要功能是從一個域名下取得 sessionID,然後將這個 sessionID 同步到另外一個域名下。這個 sessionID 其實就是一個 Cookie,相當於我們經常遇到的 JSESSIONID,所以要實現兩個域名下的 Session 同步,必須要將同一個 sessionID 作爲 Cookie 寫到兩個域名下。

總共 12 步,一個域名不用登錄就取到了另外一個域名下的 Session,當然這中間有些步驟還可以簡化,也可以做一些額外的工作,如可以寫一些需要的 Cookie,而不僅僅只傳一個 sessionID。

除此之外,該框架還能處理 Cookie 被盜取的問題。如您的密碼沒有丟失,但是您的賬號卻有可能被別人登錄的情況,這種情況很可能就是因爲您登錄成功後,您的 Cookie 被別人盜取了,盜取您的 Cookie 的人將您的 Cookie 加入到他的瀏覽器,然後他就可以通過您的 Cookie 正常訪問您的個人信息了,這是一個非常嚴重的問題。在這個框架中我們可以設置一個 Session 簽名,當用戶登錄成功後我們根據用戶的私密信息生成的一個簽名,以表示當前這個唯一的合法登錄狀態,然後將這個簽名作爲一個 Cookie 在當前這個用戶的瀏覽器進程中和服務器傳遞,用戶每次訪問服務器都會檢查這個簽名和從服務端分佈式緩存中取得的 Session 重新生成的簽名是否一致,如果不一致,顯然這個用戶的登錄狀態不合法,服務端將清除這個 sessionID 在分佈式緩存中的 Session 信息,讓用戶重新登錄。

10.5 Cookie 壓縮

Cookie 是在 HTTP 的頭部,所以通常的 gzip 和 deflate 針對 HTTP Body 的壓縮不能壓縮 Cookie,如果 Cookie 量非常大,可以考慮將 Cookie 也做壓縮,壓縮方式是將 Cookie 的多個 k/v 對看成普通的文本,做文本壓縮。壓縮算法同樣可以使用 gzip 和 deflate 算法,但是需要注意的一點是,根據 Cookie 的規範,Cookie 中不能包含控制字符,僅僅只能包含 ASCII 碼爲(34 ~ 126)的可見字符。所以要將壓縮後的結果再進行轉碼,可以進行 Base32 或者 Base64 編碼。

可以配置一個 Filter 在頁面輸出時對 Cookie 進行全部或者部分壓縮,如下代碼所示:

 private void compressCookie(Cookie c, HttpServletResponse res) { 
        try { 
            ByteArrayOutputStream bos = null; 
            bos = new ByteArrayOutputStream(); 
            DeflaterOutputStream dos = new DeflaterOutputStream(bos); 
            dos.write(c.getValue().getBytes()); 
            dos.close(); 
            System.out.println("
            before compress length:" + c.getValue(). getBytes().length); 
            String compress = new sun.misc.BASE64Encoder().encode(bos. toByteArray()); 
            res.addCookie(new Cookie("compress", compress)); 
            System.out.println("after compress length:" + compress.getBytes(). length); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
 }

上面的代碼是用 DeflaterOutputStream 對 Cookie 進行壓縮的,Deflater 壓縮後再進行 BASE64 編碼,相應地用 InflaterInputStream 進行解壓。

 private void unCompressCookie(Cookie c) { 
        try { 
            ByteArrayOutputStream out = new ByteArrayOutputStream(); 
            byte[] compress = 
        new sun.misc.BASE64Decoder().decodeBuffer(new String(c.getValue().getBytes())); 
            ByteArrayInputStream bis = new ByteArrayInputStream(compress); 
            InflaterInputStream inflater = new InflaterInputStream(bis); 
            byte[] b = new byte[1024]; 
            int count; 
            while ((count = inflater.read(b)) >= 0) { 
                out.write(b, 0, count); 
            } 
            inflater.close(); 
            System.out.println(out.toByteArray());; 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
 }

2KB 大小的 Cookie 壓縮前與壓縮後字節數相差 20% 左右,如果您的網站的 Cookie 在 2KB~3KB 左右,一天有 1 億的 PV,那麼一天就能夠產生 4TB 的帶寬流量了,從節省帶寬成本來說壓縮還是很有必要的。

10.6 表單重複提交問題

網站中在很多地方都有表單重複提交問題,一種情況是用戶在網速慢的情況下可能會重複提交表單,還有就是惡意用戶通過程序來發送惡意請求,在這些情況下都要設計一個防止表單重複提交的機制。

要能夠防止表單重複提交,就要標識用戶的每一次訪問請求,使得每一次訪問對服務端來說都是唯一確定的。爲了標識用戶的每次訪問請求,可以在用戶請求一個表單域時增加一個隱藏表單項,這個表單項的值每次都是唯一的 token,如:

 <form id=”form” method=”post”> 
 <input type=hidden name=“crsf_token” value=“xxxx”/> 
 </form>

當用戶在請求時生成這個唯一的 token 時,同時將這個 token 保存在用戶的 Session 中,等用戶提交請求時檢查這個 token 和當前的 Session 中保存的 token 是否一致。如果一致,說明沒有重複提交,否則用戶提交上來的 token 已經不是當前的這個請求的合法 token。其工作過程如圖 10-12 所示。

圖 10-12.工作過程

圖 10-12 是用戶發起對錶單頁面的請求過程,生成唯一的 token 需要一個算法,最簡單的就是可以根據一個種子作爲 key 生成一個隨機數,並保存在 Session 中,等下次用戶提交表單時做驗證。驗證表單的過程如圖 10-13 所示。

圖 10-13.驗證表單的過程

當用戶提交表單時會將請求時生成的 token 帶回來,這樣就可以和 Session 中保存的 token 做對比,從而確認這次表單驗證是否合法。

10.7 總結

Cookie 和 Session 都是爲了保持用戶訪問的連續狀態,之所以要保持這種狀態,一方面是爲了方便業務實現,另一方面就是簡化服務端程序設計,提高訪問性能,但是這也帶來了另外一些挑戰,如安全問題、應用的分佈式部署帶來的 Session 的同步問題及跨域名 Session 的同步等一系列問題。本章分析了 Cookie 和 Session 的工作原理,並介紹了一致分佈式 Session 的解決方案。

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