天天看點

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

        Session 與 Cookie 不管是對 Java Web 的初學者還是熟練使用者來說都是一個令人頭疼的問題。在初入職場時恐怕很多程式員在面試時都被問過這個問題。其實這個問題回答起來既簡單又複雜,簡單是因為它們本身隻是 HTTP 中的一個配置項,在 Servlet 規範中也隻是對應到一個類而已;說它複雜原因在于當我們的系統大到需要用到很多 Cookie 時,我們不得不考慮 HTTP 對 Cookie 數量和大小的限制,那麼如何才能解決這個瓶頸呢?Session 也會有同樣的問題,當我們的一個應用系統有幾百台伺服器時,如何解決 Session在多台伺服器之間共享的問題?它們還有一些安全問題,如 Cookie 被盜、Cookie 僞造等問題應如何避免。本章将詳細解答這些問題,同時也将分享淘寶在解決這些問題時總結的一些經驗。

        Session 與 Cookie 的作用都是為了保持通路使用者與後端伺服器的互動狀态。它們有各自的優點,也有各自的缺陷,然而具有諷刺意味的是它們的優點和它們的使用場景又是沖突的。例如,使用 Cookie 來傳遞資訊時,随着 Cookie 個數的增多和通路量的增加,它占用的網絡帶寬也很大,試想假如 Cookie 占用 200 個位元組,如果一天的 PV 有幾億,那麼它要占用多少帶寬?是以有大通路量時希望用 Session,但是 Session 的緻命弱點是不容易在多台伺服器之間共享,這也限制了 Session 的使用。 

了解 Cookie 

        Cookie 的作用我想大家都知道,通俗地說就是當一個使用者通過 HTTP 通路一個伺服器時,這個伺服器會将一些 Key/Value 鍵值對傳回給用戶端浏覽器,并給這些資料加上一些限制條件,在條件符合時這個使用者下次通路這個伺服器時,資料又被完整地帶回給伺服器。

    這個作用就像你去超市購物時,第一次給你辦張購物卡,在這個購物卡裡存放了一些你的個人資訊,下次你再來這個連鎖超市時,超市會識别你的購物卡,下次直接購物就好了。

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

Cookie 屬性項 

    目前 Cookie 有兩個版本:Version 0 和 Version 1,它們有兩種設定響應頭的辨別,分别是“Set-Cookie”和“Set-Cookie2”。這兩個版本的屬性項有些不同,表 10-1 和表 10-2是對這兩個版本的屬性介紹。 

表 10-1 Version 0 屬性項介紹 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

表 10-2 Version 1 屬性項介紹 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

    在以上兩個版本的 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。 

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 響應頭的時序圖。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

    從圖 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 所示。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

    在 Cookie 項中可以詳細檢視 Cookie 屬性項,如圖 10-3 所示。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

    前面主要介紹了在服務端如何建立 Cookie,下面看一下如何從用戶端擷取 Cookie。

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

使用 Cookie 的限制 

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

表 10-3 浏覽器對 Cookie 的大小和數量的限制 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 
javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

了解 Session 

    前面已經介紹了 Cookie 可以讓服務端程式跟蹤每個用戶端的通路,但是每次用戶端的通路都必須傳回這些 Cookie,如果 Cookie 很多,則無形地增加了用戶端與服務端的資料傳輸量,而 Session 的出現正是為了解決這個問題。

    同一個用戶端每次和服務端互動時,不需要每次都傳回所有的 Cookie 值,而是隻要傳回一個 ID,這個 ID 是用戶端第一次通路伺服器時生成的,而且每個用戶端是唯一的。這樣每個用戶端就有了一個唯一的 ID,用戶端隻要傳回這個 ID 就行了,這個 ID 通常是NANE 為 JSESIONID 的一個 Cookie。 

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。 

Session 如何工作 

    有了 Session ID,服務端就可以建立 HttpSession 對象了,第一次觸發通過request.getSession()方法。如果目前的Session ID還沒有對應的HttpSession對象,那麼就建立一個新的,并将這個對象加到 org.apache.catalina. Manager 的 sessions 容器中儲存。Manager 類将管理所有 Session 的生命周期,Session 過期将被回收,伺服器關閉,Session将被序列化到磁盤等。隻要這個 HttpSession 對象存在,使用者就可以根據 Session ID 來擷取這個對象,也就做到了對狀态的保持。

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

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

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

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 
javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 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 的類關系圖。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

        StandardManager 類負責 Servlet 容器中所有的 StandardSession 對象的生命周期管理。當 Servlet 容器重新開機或關閉時,StandardManager 負責持久化沒有過期的 StandardSession 對象,它會将所有的 StandardSession 對象持久化到一個以“SESSIONS.ser”為檔案名的檔案中。到 Servlet 容器重新開機時,也就是 StandardManager 初始化時,它會重新讀取這個檔案,解析出所有 Session 對象,重新儲存在 StandardManager 的 sessions 集合中。Session 的恢複狀态圖如圖 10-7 所示。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 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 中這個有效時間是 60s(maxInactiveInterval 屬性控制),超過 60s 該 Session 将會過期。檢查每個 Session 是否失效是在 Tomcat 的一個背景線程中完成的(backgroundProcess()方法中)。過期 Session 的狀态圖如圖 10-8 所示。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 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 對象是否存在。 

Cookie 安全問題 

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

    相比較而言 Session 的安全性要高很多,因為 Session 是将資料儲存在服務端,隻是通過 Cookie 傳遞一個 SessionID 而已,是以 Session 更适合存儲使用者隐私和重要的資料。 

分布式 Session 架構 

    從前面的分析可知,Session 和 Cookie 各自有優點和缺點。在大型網際網路系統中,單獨使用 Cookie 和 Session 都是不可行的,原因很簡單。因為如果使用 Cookie,則可以很好地解決應用的分布式部署問題,大型網際網路應用系統的一個應用有上百台機器,而且有很多不同的應用系統協同工作,由于 Cookie 是将值存儲在用戶端的浏覽器裡,使用者每次通路都會将最新的值帶回給處理該請求的伺服器,是以也就解決了同一個使用者的請求可能不在同一台伺服器處理而導緻的 Cookie 不一緻的問題。 

存在哪些問題 

    這種“誰家的孩子誰抱走”的處理方式的确是大型網際網路的一個比較簡單但的确可以解決問題的處理方式,但是這種處理方式也會帶來了很多其他問題,如下所述。

  • ◎  用戶端 Cookie 存儲限制。随着應用系統的增多,Cookie 數量也快速增加,但浏覽器對于使用者 Cookie 的存儲是有限制的。例如,對 IE7 之前的 IE 浏覽器,Cookie個數的限制是 20 個;而對後續的版本,包括 Firefox 等,Cookie 個數的限制都是50 個,總大小不超過 4KB,超過限制就會出現丢棄 Cookie 的現象,這會嚴重影響應用系統的正常使用。
  • ◎  Cookie 管理的混亂。在大型網際網路應用系統中,如果每個應用系統都自己管理每個應用使用的 Cookie,則會導緻混亂,由于通常應用系統都在同一個域名下,Cookie 又有上面一條提到的限制,是以沒有統一管理很容易出現 Cookie 超出限制的情況。 
◎ 安全令人擔憂。雖然可以通過設定 HttpOnly 屬性防止一些私密 Cookie 被用戶端通路,但是仍然不能保證 Cookie 無法被篡改。為了保證 Cookie 的私密性通常會對 Cookie 進行加密,但是維護這個加密 Key 也是一件麻煩的事情,無法保證定期更新加密 Key 也是帶來安全性問題的一個重要因素。

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

可以解決哪些問題

    既然 Cookie 有以上問題,Session 也有它的好處,那麼為何不結合使用 Session 和 Cookie呢?下面是分布式 Session 架構可以解決的問題。

  • ◎  Session 配置的統一管理。
  • ◎  Cookie 使用的監控和統一規範管理。
  • ◎  Session 存儲的多元化。
  • ◎  Session 配置的動态修改。
  • ◎  Session 加密 key 的定期修改。
  • ◎  充分的容災機制,保持架構的使用穩定性。
  • ◎  Session 各種存儲的監控和報警支援。
  • ◎  Session 架構的可擴充性,相容更多的 Session 機制如 wapSession。
  • ◎  跨域名 Session 與 Cookie 如何共享的問題。現在同一個網站可能存在多個域名,如何将 Session 和 Cookie 在不同的域名之間共享是一個具有挑戰性的問題。 

總體實作思路

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

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

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

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

<session>
	<key>sessionID</key>
	<cookiekey>sessionID</cookiekey >
	<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 中,這樣即使分布式緩存出現問題也不會影響關鍵業務的正常運作。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

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

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

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

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

    除此之外,該架構還能處理 Cookie 被盜取的問題。如你的密碼沒有丢失,但是你的賬号卻有可能被别人登入的情況,這種情況很可能就是因為你登入成功後,你的 Cookie被别人盜取了,盜取你的 Cookie 的人将你的 Cookie 加入到他的浏覽器,然後他就可以通過你的 Cookie 正常通路你的個人資訊了,這是一個非常嚴重的問題。在這個架構中我們可以設定一個 Session 簽名,當使用者登入成功後我們根據使用者的私密資訊生成的一個簽名,以表示目前這個唯一的合法登入狀态,然後将這個簽名作為一個 Cookie 在目前這個使用者的浏覽器程序中和伺服器傳遞,使用者每次通路伺服器都會檢查這個簽名和從服務端分布式緩存中取得的 Session 重新生成的簽名是否一緻,如果不一緻,則顯然這個使用者的登入狀态不合法,服務端将清除這個 sessionID 在分布式緩存中的 Session 資訊,讓使用者重新登入。 

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在 2~3KB 左右,一天有 1 億的 PV,那麼一天就能夠産生 4TB 的帶寬流量了,從節省帶寬成本來說壓縮還是很有必要的。 

表單重複送出問題 

    在網站中有很多地方都存在表單重複送出的問題,如使用者在網速慢的情況下可能會重複送出表單,又如惡意使用者通過程式來發送惡意請求等,這時都需要設計一個防止表單重複送出的機制。

    要防止表單重複送出,就要辨別使用者的每一次通路請求,使得每一次通路對服務端來說都是唯一确定的。為了辨別使用者的每次通路請求,可以在使用者請求一個表單域時增加一個隐藏表單項,這個表單項的值每次都是唯一的 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 所示。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

    圖 10-12 是使用者發起的對表單頁面的請求過程,生成唯一的 token 需要一個算法,最簡單的就是可以根據一個種子作為 key 生成一個随機數,并儲存在 Session 中,等下次使用者送出表單時做驗證。驗證表單的過程如圖 10-13 所示。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

    當使用者送出表單時會将請求時生成的 token 帶回來,這樣就可以和在 Session 中儲存的 token 做對比,進而确認這次表單驗證是否合法。 

多終端 Session 統一 

    目前大部分網站都有了無線端,對無線端的 Cookie 如何處理也是很多程式員必須考慮的問題。

    在無線端發展初期,後端的服務系統未必和 PC 的服務系統是統一的,這樣就涉及在一端調用多個系統時如何做到服務端 Session 共享的問題了。有兩個明顯的例子:一個是在無線端可能會通過手機通路無線服務端系統,同時也會通路 PC 端的服務系統,如果它們兩個的登入系統沒有統一的話,将會非常麻煩,可能會出現二次登入的情況;另一個是在手機上登入以後再在 PC 上同樣通路服務端資料,Session 能否共享就決定了用戶端是否要再次登入。

    針對這兩種情況,目前都有理想的解決方案。

        1)多端共享 Session

    多端共享 Session 必須要做的工作是不管是無線端還是 PC 端,後端的服務系統必須統一會話架構,也就是兩邊的登入系統必須要基于一緻的會員資料結構、Cookie 與 Session的統一。也就是不管是 PC 端登入還是無線端登入,後面對應的資料結構和存儲要統一,寫到用戶端的 Cookie 也必須一樣,這是前提條件。 

    那麼如何做到這一點?就是要按照我們在前面所說的實作分布式的 Session 架構。如下圖 10-14 所示。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

    上面服務端統一 Session 後,在同一個終端上不管是通路哪個服務端都能做到登入狀态統一。例如不管是Native還是内嵌Webview,都可以拿統一的Session ID去服務端驗證登入狀态。

        2)多終端登入

    目前很多網站都會出現無線端和 PC 端多端登入的情況,例如可以通過掃碼登入等。這些是如何實作的呢?其實比較簡單,如圖 10-15 所示。 

javaEE 深入了解 Session 與 Cookie了解 Cookie 了解 Session Cookie 安全問題 分布式 Session 架構 Cookie 壓縮 表單重複送出問題 多終端 Session 統一 總結 

    這裡手機端在掃碼之前必須是已經登入的狀态,因為這樣才能擷取到底是誰将要登入的資訊,同時掃碼的二維碼也帶有一個特定的辨別,辨別是這個用戶端通過手機端登入了。當手機端掃碼成功後,會在服務端設定這個二維碼對應的辨別為已經登入成功,這時 PC用戶端會通過将“心跳”請求發送到服務端,來驗證是否已經登入成功,這樣就成為一種便捷的登入方式。 

總結 

        Cookie 和 Session 都是為了保持使用者通路的連續狀态,之是以要保持這種狀态,一方面是為了友善業務實作,另一方面就是簡化服務端的程式設計,提高通路性能,但是這也帶來了另外一些挑戰,例如安全問題、應用的分布式部署帶來的 Session 的同步問題及跨域名 Session 的同步問題等。本章分析了 Cookie 和 Session 的工作原理,并介紹了一種分布式 Session 的解決方案。