黃湘平 , 進階軟體工程師,IBM CSDL
2005 年 11 月 10 日
讀者定位為具有Java和Web開發經驗的開發和設計人員。
讀者可以學習到關于Cookie的工作原理和Cookie協定的細節,以及在一個HTTP應用代理的場景下Cookie的管理和處理思想,并可以直接使用文中的代碼和思路,提高工作效率。
随着越來越多的系統移植到了Web上,HTTP協定具有了比以前更廣泛的應用。不同的系統對WEB實作提出了不同的要求,基于HTTP協定的網絡應用正趨于複雜化和多元化。很多應用需要把使用者請求的頁面進行處理後再傳回給使用者,比如頁面關鍵字過濾,頁面内容緩存、内容搜尋、頁面翻譯等等。這些應用在實際效果上類似于一個HTTP應用代理:它們首先接受使用者的請求,根據使用者請求的URL去真正的目标伺服器取回目标頁面,再根據不同應用的要求做出相應處理後傳回給使用者。這樣使用者直接面對的就是這個HTTP應用代理,而通過它與其他頁面進行互動。Cookie或Session技術的應用,解決了HTTP協定的一個問題 -- 無法保持客戶狀态,是以它現在被廣泛應用于各種Web站點中。上面提到的那些應用如果不能處理好Cookie和Session的傳遞、更新和廢除等問題,就會極大的限制它們所能處理站點的範圍,是以如何在HTTP應用代理中正确處理Cookie,成為一個必須解決的問題。本文結合在頁面翻譯(Machine Translation System)項目中對于Cookie的處理方法,探讨一下這方面的解決方案。
MTS項目簡介及讨論前提
Machine Translation System(以下簡稱MTS)是一個線上實時頁面翻譯系統,為使用者線上提供把英文頁面翻譯成其他9種語言的服務。使用者通過向MTS系統送出一個類似下面的URL使用此服務,其中參數url指明了使用者所需要翻譯的目标位址,參數language指明了所需翻譯成的目智語言,www.mts.com是假想中提供MTS服務的站點。
HTTP://www.mts.com/translate?url=http://www.ibm.com/&language=French
一個完整的MTS系統處理過程可以分解成以下幾個步驟:
- 使用者向MTS送出合适的URL。
- MTS在接到使用者的請求後,解析出使用者需要翻譯的目标位址和目智語言,根據使用者請求的目标位址,把請求轉發到目标伺服器。
- MTS接受來自目标伺服器的應答,包括頁面資訊和HTTP頭資訊。
- MTS在确定得到正确的目标頁面後,把頁面内容送入WebSphere Translation Server進行翻譯。
- 把翻譯後的頁面連同修改後的HTTP頭資訊送出給使用者。
當然,這其中涉及到很多的應用處理。比如與各種HTTP/HTTPS站點建立聯結、根據HTTP頭資訊進行頁面跳轉和錯誤處理、為始終保持使用者在翻譯模式下而對目标的HTML頁面進行分析和修改,根據系統設定對某些DNT(Do Not Translate)的頁面進行過濾和跳轉,當然還有對Cookie的處理等等。其他問題跟這篇文章關聯不大,我們重點讨論在這種情況下的Cookie處理。Cookie跟随目标伺服器的HTTP頭資訊被MTS接收到,經過MTS整理之後發給用戶端浏覽器。MTS在接到下一次使用者對同一個站點的翻譯請求時,再把從用戶端得到的Cookie發送給目标伺服器。
在以上的場景中,MTS充當的作用類似于一種HTTP應用代理伺服器,它代替使用者取得目标頁面,并在作出相應處理後再送出給使用者。當然,這種代理伺服器不需要使用者修改浏覽器的代理伺服器參數或者網絡配置,而隻是簡單的在浏覽器的位址欄中輸入一個MTS能夠識别的URL即可。此篇文章也是在這樣一個應用場景的基礎上,展開對HTTP應用代理伺服器如何處理Cookie的讨論。
問題的産生
在MTS系統中,目标伺服器的Cookie在兩個地方會産生問題。當MTS接收目标伺服器應答的時候,Cookie随着HTTP頭資訊被MTS接收到的。這時候目标伺服器認為MTS就是最終客戶,是以它賦予了Cookie與目标伺服器相符的屬性。而如果MTS把這些Cookie原封不動的儲存在HTTP頭資訊中,傳給真正的最終使用者的話,使用者的浏覽器會因為這些Cookie不合法而忽略它們。同理,當Cookie從浏覽器端傳回目标伺服器的時候,也會遇到相同的問題。是以有必要對Cookie進行一些處理,以保證使用者的浏覽器能真正識别和利用這些Cookie。
但是為何使用者浏覽器無法識别從目标伺服器傳過來的原始Cookie呢?這是因為出于安全性的考慮,Cookie規範制定的時候對Cookie的産生和接受設定了一些嚴格的規範,不符合這些規範的Cookie,浏覽器和伺服器都将予以忽略。下面我們從Cookie規範入手進行介紹。
Cookie的規範介紹
目前有以下幾種Cookie規範:
- Netscape cookie草案:是最早的cookie規範,基于rfc2109。盡管這個規範與rc2109有較大的差别,但是很多伺服器都與之相容。
- rfc2109, 是w3c釋出的第一個官方cookie規範。理論上講,所有的伺服器在處理cookie(版本1)時,都要遵循此規範。遺憾的是,這個規範太嚴格了,以緻很多伺服器不正确的實施了該規範或仍在使用Netscape規範。
- rfc2965規範定義了cookie版本2,并說明了cookie版本1的不足。
rfc2965規範的使用,目前并不多。rfc2109規範相應要嚴格得多,在實際應用上,并不是所有的浏覽器和Web伺服器都嚴格遵守。是以相比較而言,Netscape cookie草案倒是一個比較簡潔和被廣泛支援的Cookie規範,是以我們在這裡以Netscape cookie草案為基礎進行讨論,對于其他兩種規範,我們的讨論和代碼具有相同的意義。關于Netscape cookie草案的細節,大家可以參照Netscape官方站點,這裡我們列舉一些和我們讨論有關的内容。
根據Netscape cookie草案的描述,Cookie 是Web 伺服器向使用者的浏覽器發送的一段ASCII碼文本。一旦收到Cookie,浏覽器會把Cookie的資訊片斷以"名/值"對(name-value pairs)的形式儲存儲存在本地。這以後,每當向同一個Web 伺服器請求一個新的文檔時,Web 浏覽器都會發送之站點以前存儲在本地的Cookie。建立Cookie的最初目的是想讓Web伺服器能夠通過多個HTTP請求追蹤客戶。有些複雜的網絡應用需要在不同的網頁之間保持一緻,它們需要這種會話狀态的保持能力。
浏覽器與Web伺服器通過HTTP協定進行通訊,而Cookie就是儲存在HTTP協定的請求或者應答頭部(在HTTP協定中,資料包括兩部分,一部分是頭部,由一些名值對構成,用來描述要被傳輸資料的一些資訊。一部分是主體(body),是真正的資料(如HTML頁面等))進行傳送的。
在HTML文檔被發送之前,Web伺服器通過傳送HTTP 標頭中的Set-Cookie 消息把一個cookie 發送到使用者的浏覽器中。下面是一個遵循Netscape cookie草案的完整的Set-Cookie 頭:
|
Set-Cookie的每個屬性解釋如下:
- Customer=huangxp 一個"名稱=值"對,把名稱customer設定為值"huangxp",這個屬性在Cookie中必須有。
- path=/foo 控制哪些通路能夠觸發cookie 的發送。如果沒有指定path,cookie 會在所有對此站點的HTTP 傳送時發送。如果path=/directory,隻有通路/directory 下面的網頁時,cookie才被發送。在這個例子中,使用者在通路目錄/foo下的内容時,浏覽器将發送此cookie。如果指定了path,但是path與目前通路的url不符,則此cookie将被忽略。
- domain=.ibm.com 指定cookie被發送到哪台計算機上。正常情況下,cookie隻被送回最初向使用者發送cookie 的計算機。在這個例子中,cookie 會被發送到任何在.ibm.com域中的主機。如果domain 被設為空,domain 就被設定為和提供cookie 的Web 伺服器相同。如果domain不為空,并且它的值又和提供cookie的Web伺服器域名不符,這個Cookie将被忽略。
- expires= Wednesday, 19-OCT-05 23:12:40 GMT 指定cookie 失效的時間。如果沒有指定失效時間,這個cookie 就不會被寫入計算機的硬碟上,并且隻持續到這次會話結束。
- secure 如果secure 這個詞被作為Set-Cookie 頭的一部分,那麼cookie 隻能通過安全通道傳輸(目前即SSL通道)。否則,浏覽器将忽略此Cookie。
一旦浏覽器接收了cookie,這個cookie和對遠端Web伺服器的連續請求将一起被浏覽器發送。例如 前一個cookie 被存入浏覽器并且浏覽器試圖請求URL http://www.ibm.com/foo/index.html 時,下面的HTTP 標頭就被發送到遠端的Web伺服器。
GET /foo/index.html HTTP/1.0
Cookie:customer=huangxp
一次典型的網絡浏覽過程
在了解了Cookie協定的一些基本内容之後,讓我們看看一次典型的網絡浏覽過程中浏覽器如何識别和處理Cookie:
-
浏覽器對于Web伺服器應答標頭中Cookie的操作步驟:
1. 從Web伺服器的應答標頭中提取所有的cookie。
2. 解析這些cookie的組成部分(名稱,值,路徑等等)。
3. 判定主機是否允許設定這些cookie。允許的話,則把這些Cookie存儲在本地。
-
浏覽器對Web伺服器請求標頭中所有的Cookie進行篩選的步驟:
1. 根據請求的URL和本地存儲cookie的屬性,判斷那些Cookie能被發送給Web伺服器。
2. 對于多個cookie,判定發送的順序。
3. 把需要發送的Cookie加入到請求HTTP標頭中一起發送。
由MTS代理的網絡浏覽過程
以上我們了解了在一個典型的浏覽器與Web伺服器互動的時候,Cookie的傳遞過程。下面我們将看到,如果在MTS代理網絡浏覽的過程中,不對Cookie進行修改,上面的Cookie傳遞過程将無法實作。
1. 假設使用者希望把http://www.ibm.com/foo/index.html頁面翻譯成法文,應該使用如下的url對MTS送出請求
:
HTTP://www.mts.com/translate?url=http://www.ibm.com/foo/index.html&language=French
2. MTS接收使用者的請求,連接配接遠端目标伺服器http://www.ibm.com/foo/index.html。目标伺服器做出應答,傳回HTTP頭和HTML頁面内容。其中,典型的HTTP頭内容如下:
|
3. MTS不對Set-Cookie後的内容作任何處理,直接把它加到使用者浏覽器的應答頭上發送給浏覽器。
4. 浏覽器将從Set-Cookie中解析出domain和path的值,分别是.ibm.com和/foo,并與請求的url:HTTP://www.mts.com/translate?url=http://www.ibm.com/foo/index.html&language=French進行比較。請求url的domain是www.mts.com,path是/,與Set-Cookie中的屬性不符,是以浏覽器将忽略此Cookie。
另外,在浏覽器發送Cookie的時候也會遇到同樣的問題,同樣如上例,如果浏覽器裡本來已經存儲了http://www.ibm.com/foo/的Cookie,但由于使用者要通過MTS通路此站點,浏覽器經不會把已經存儲的Cookie上轉到MTS中,MTS也就無法把之傳遞到http://ibm.com/foo/上。
基于上面Cookie規範的介紹和例證,我們能看出,浏覽器在接受某一個站點的Cookie的時候,需要檢查Cookie的參數domain、path、secure,看是否與目前的站點和URL相符,如果不符的話,就會忽略。另一方面。浏覽器在上傳Cookie的時候,也會根據目前所通路站點的屬性,上傳相關的Cookie,而其他的Cookie則不予上傳。
至此,我們讨論了需要修改Cookie的根本原因在于Cookie規範的限制。下面我們讨論兩種解決問題的思路。
解決問題的兩種思路
Cookie的存在是要解決HTTP協定本身先天的缺陷-無狀态性,它為使用者儲存了一些需要的狀态資訊。是以我們解決此問題的最本質的出發點,也就是找到一種途徑能為使用者儲存Cookie所提供使用者狀态資訊,實際上就是Name/Value對。
思路一
第一種思路就是修改目标伺服器取得的Cookie,使之符合MTS站點的屬性,然後作為MTS站點的Cookie存儲到使用者的浏覽器中去。當然,這種修改必須保留原始Cookie的所有屬性值,當以後通路同一個目标伺服器的時候,MTS能根據儲存的屬性值還原出原始Cookie,然後進行送出。
具體到屬性值的儲存位置,沒有太多選擇的餘地,實際上,domain,path,secure,expires這幾個屬性都無法利用,隻有利用name=value這一屬性對。我們的做法是創造一個新的Cookie,把原始Cookie的domain,path的值與name值進行編碼,用分隔符附加在Name值的後面,符值給新的Cookie。這樣做也同時避免了不同目标伺服器如果出現同名的Cookie,将會互相覆寫的情況(Cookie規範裡面也規定了,用戶端以domain,path,name作為Cookie的唯一标示)。而原始Cookie的secure和expires值,直接符給新的Cookie,新Cookie的domain和path設成預設值,這樣,新Cookie就可以被浏覽器正常接受。由于浏覽器接受的所有Cookie的domain和path值都一樣,是以每次使用者對MTS提出請求時,浏覽器都會把所有與MTS站點相關的Cookie上傳,是以,MTS還需要還原原始的Cookie,過濾掉與目标伺服器不相幹的Cookie,然後上傳有用的Cookie。
這種思路的優點在于Cookie存儲在用戶端,可以做到長期存儲,浏覽器自己根據Cookie的expires值做出判斷,省掉很多開發的麻煩。缺點是轉換的過程相對較複雜。另外還有一個缺點,也是由于Cookie規範的限制所造成的。Cookie規範對于一個浏覽器同時能夠存儲的Cookie數量作出了規定。
- 總共300 個cookie
- 每個Cookie 4 K 的存儲容量
- 每一個domain 或者 server 20 個cookie。
以上是浏覽器所應達到的最小存儲數量,超出這個限制,浏覽器應該自動按照最少最近被使用的原則删除超出得Cookie。由于使用者有可能通過MTS這一個網站翻譯大量的目标伺服器,是以浏覽器存儲在MTS的domain下的cookie數量就很有可能超過20個,這時候就會導緻某些Cookie被删除。一般這也不會造成太大問題,因為規範是要求浏覽器删除最少最近被使用的Cookie,但我們在實際測試當中發現有些浏覽器并不遵守這樣的規範,而是删除最新的Cookie,這就将導緻使用者很大的不便。
思路二
第二種思路在于把原始的Cookie組織成dataBean,存儲到使用者的Session當中去。這樣,在使用者端隻需要存儲一個SessionID的Cookie,而不需要存儲所有目标伺服器的每一個Cookie。另外,當接收到使用者的又一次翻譯請求時,再從Session當中取出所有的dataBean,逐一進行分析,找出與使用者所請求的目标伺服器相符的原始Cookie,進行送出。
這種思路可以克服上一種思路中Cookie超過标準數量時的缺陷,而且不需編碼儲存原始的Cookie屬性值,減少了程式的複雜度。缺點是需要程式員自己處理expires。而且由于是把Cookie存儲在Session中,一旦Session失效,所有Cookie都将被删除,是以,無法儲存那些長期的Cookie。
總之,兩種思路各有利弊,在實際應用當中要權衡考慮。下面我們針對兩種思路進行技術實作,分别對應方案一和方案二。
由于MTS需要與目标伺服器連接配接,遵循HTTP協定讀取和傳回Cookie,但是如果用JDK中的java.net.URLConnection處理Cookie将非常不友善,是以我們使用HTTPClient來處理與目标伺服器的連接配接。
方案一:Cookie存儲在浏覽器端
使用者每發起一次新的請求,浏覽器在檢查完本地存儲Cookie的有效性後,會把所有由MTS産生的有效Cookie附加在請求頭裡送到MTS。MTS接受到用戶端的翻譯請求後,從Request中提取出所有的Cookie,還原後根據目标伺服器的domain和path進行過濾。産生所有與目标伺服器相關的Cookie。
|
接下來,需要把Cookie送到目标伺服器中。我們使用HTTPClient與目标伺服器連接配接。HTTPClient在與目标伺服器連接配接以後,允許伺服器設定Cookie并在需要的時候自動将Cookie傳回伺服器,也支援手工設定 Cookie後發送到伺服器端。但是,由于如何處理cookie有幾個規範互相沖突:Netscape Cookie 草案、RFC2109、RFC2965,而且還有很大數量的軟體商的Cookie實作不遵循任何規範。 為了處理這種狀況,需要把HttpClient設定成Cookie相容模式,這樣可以最大限度的處理好各種Cookie。下面的代碼把Cookie送到目标伺服器。
|
MTS把請求和Cookie送出後,繼續接收目标伺服器的應答,讀取傳回的原始Cookie,并轉換成可以存儲在使用者浏覽器端的Cookie。下面的代碼将對原始Cookie的内容進行變換,保留expires和secure等項,把domain和path項編碼到name中去。
|
最後一步,把這些Cookie儲存到response裡,随HTTP應答頭傳回使用者浏覽器。并儲存在浏覽器中。
|
至此,我們已經完成了接收使用者請求,轉換Cookie,發送到目标伺服器,接收目标伺服器的原始Cookie,并儲存在客戶浏覽器的整個處理過程。
方案二:Cookie存儲在伺服器端
在此種方案中,目标伺服器傳回給MTS的Cookie将被組織成dataBean,存儲在使用者的Session中。是以,我們首先生成一個用來存儲Cookie的類CookiesBean,根據它的特性,它可以繼承ArraryList類。此對象将存儲使用者通路目标伺服器時接收到的所有Cookie,并提供與新接收到的Cookie融合的功能,同時能夠删除過期的Cookie,更新同名的Cookie。
|
當MTS接受到用戶端的翻譯請求後,會從Session中提取出所有的dataBean,并得到存儲的所有Cookie。如以下代碼:
|
MTS在所有的存儲的Cookie中,檢查Cookie的Domain、path和secure的值,篩選出符合目标伺服器的Cookie。
|
把Cookie送到目标伺服器的代碼與方案一基本一樣,在此忽略。
最後一步,需要把Cookie存儲到Session中。下面的代碼将從目标伺服器接受Cookie,融入到dataBean中,并儲存到客戶的Session中。
|
至此,我們已經完成了在Session中儲存個目标伺服器所産生Cookie的整個處理過程。
關于Session的考慮
在研究完如何管理和傳遞Cookie之後,我們也需要研究一下Session的傳遞。因為目前大部分站點都在采用Session機制儲存使用者狀态資料,如果不能解決Session的傳遞問題,HTTP應用代理伺服器的适用範圍同樣會大打折扣。
首先我們了解一下Session的實作機制。Session是一種伺服器端的機制,伺服器使用一種類似于散清單的結構來儲存資訊。當程式需要為某個用戶端的請求建立一個session的時候,伺服器首先檢查這個用戶端的請求裡是否已包含了一個session辨別 - 稱為session id,如果已包含一個session id則說明以前已經為此用戶端建立過session,伺服器就按照session id把這個session檢索出來使用(如果檢索不到,可能會建立一個),session id的值應該是一個既不會重複,又不容易被找到規律以仿造的字元串。
儲存這個session id的方式之一就是采用Cookie。一般這個Cookie的名字都類似于 SESSIONID。比如WebSphere對于Web應用程式生成的Cookie:JSESSIONID= 0001HWF4iVD94pY8Cpbx6U4CXkf:10lro0398,它的名字就是 JSESSIONID。
儲存session id的其他方式還包括URL重寫和表單隐藏字段。這兩種方式都不需要代理伺服器作特殊處理。是以實際上,我們解決了Cookie的管理和傳遞的問題之後,也就解決了Session的管理和傳遞。
結束語
從上面的讨論中可以看出,由于Cookie本身的規範限制,HTTP應用代理所必需面對的一個問題就是如何對Cookie進行正确的處理。本文對此提出了兩種解決思路并列出了實作代碼。對于MTS項目本身,我們使用的是第二種方案。開發人員在認識好Cookie本身的特性之後,參照本文的思路,根據自己系統的特點,也會找出更适宜的解決方案。
參考資料
- Netscape Cookie Specification 對Netscape Cookie 使用的特性進行了簡要的介紹。
- RFC2965:HTTP State Management Mechanism 介紹了HTTP 狀态管理機制
- RFC2109 w3c釋出的第一個官方cookie規範
- RFC2616:Hypertext Transfer Protocol 超文本傳輸協定
- Ronald Tschalr開發了 HTTPClient,将其作為 URLConnection 的替代品。
- Jakarta Regexp Apache 的開源項目,處理正規表達式的java包。