黃湘平
, 高級(jí)軟件工程師,IBM CSDL
2005 年 11 月 10 日
讀者定位為具有Java和Web開(kāi)發(fā)經(jīng)驗(yàn)的開(kāi)發(fā)和設(shè)計(jì)人員。
讀者可以學(xué)習(xí)到關(guān)于Cookie的工作原理和Cookie協(xié)議的細(xì)節(jié),以及在一個(gè)HTTP應(yīng)用代理的場(chǎng)景下Cookie的管理和處理思想,并可以直接使用文中的代碼和思路,提高工作效率。
隨著越來(lái)越多的系統(tǒng)移植到了Web上,HTTP協(xié)議具有了比以前更廣泛的應(yīng)用。不同的系統(tǒng)對(duì)WEB實(shí)現(xiàn)提出了不同的要求,基于HTTP協(xié)議的網(wǎng)絡(luò)應(yīng)用正趨于復(fù)雜化和多元化。很多應(yīng)用需要把用戶請(qǐng)求的頁(yè)面進(jìn)行處理后再返回給用戶,比如頁(yè)面關(guān)鍵字過(guò)濾,頁(yè)面內(nèi)容緩存、內(nèi)容搜索、頁(yè)面翻譯等等。這些應(yīng)用在實(shí)際效果上類似于一個(gè)HTTP應(yīng)用代理:它們首先接受用戶的請(qǐng)求,根據(jù)用戶請(qǐng)求的URL去真正的目標(biāo)服務(wù)器取回目標(biāo)頁(yè)面,再根據(jù)不同應(yīng)用的要求做出相應(yīng)處理后返回給用戶。這樣用戶直接面對(duì)的就是這個(gè)HTTP應(yīng)用代理,而通過(guò)它與其他頁(yè)面進(jìn)行交互。Cookie或Session技術(shù)的應(yīng)用,解決了HTTP協(xié)議的一個(gè)問(wèn)題 -- 無(wú)法保持客戶狀態(tài),因此它現(xiàn)在被廣泛應(yīng)用于各種Web站點(diǎn)中。上面提到的那些應(yīng)用如果不能處理好Cookie和Session的傳遞、更新和廢除等問(wèn)題,就會(huì)極大的限制它們所能處理站點(diǎn)的范圍,因此如何在HTTP應(yīng)用代理中正確處理Cookie,成為一個(gè)必須解決的問(wèn)題。本文結(jié)合在頁(yè)面翻譯(Machine Translation System)項(xiàng)目中對(duì)于Cookie的處理方法,探討一下這方面的解決方案。
MTS項(xiàng)目簡(jiǎn)介及討論前提
Machine Translation System(以下簡(jiǎn)稱MTS)是一個(gè)在線實(shí)時(shí)頁(yè)面翻譯系統(tǒng),為用戶在線提供把英文頁(yè)面翻譯成其他9種語(yǔ)言的服務(wù)。用戶通過(guò)向MTS系統(tǒng)提交一個(gè)類似下面的URL使用此服務(wù),其中參數(shù)url指明了用戶所需要翻譯的目標(biāo)地址,參數(shù)language指明了所需翻譯成的目標(biāo)語(yǔ)言,www.mts.com是假想中提供MTS服務(wù)的站點(diǎn)。
HTTP://www.mts.com/translate?url=http://www.ibm.com/&language=French
一個(gè)完整的MTS系統(tǒng)處理過(guò)程可以分解成以下幾個(gè)步驟:
- 用戶向MTS提交合適的URL。
- MTS在接到用戶的請(qǐng)求后,解析出用戶需要翻譯的目標(biāo)地址和目標(biāo)語(yǔ)言,根據(jù)用戶請(qǐng)求的目標(biāo)地址,把請(qǐng)求轉(zhuǎn)發(fā)到目標(biāo)服務(wù)器。
- MTS接受來(lái)自目標(biāo)服務(wù)器的應(yīng)答,包括頁(yè)面信息和HTTP頭信息。
- MTS在確定得到正確的目標(biāo)頁(yè)面后,把頁(yè)面內(nèi)容送入WebSphere Translation Server進(jìn)行翻譯。
- 把翻譯后的頁(yè)面連同修改后的HTTP頭信息提交給用戶。
當(dāng)然,這其中涉及到很多的應(yīng)用處理。比如與各種HTTP/HTTPS站點(diǎn)建立聯(lián)結(jié)、根據(jù)HTTP頭信息進(jìn)行頁(yè)面跳轉(zhuǎn)和錯(cuò)誤處理、為始終保持用戶在翻譯模式下而對(duì)目標(biāo)的HTML頁(yè)面進(jìn)行分析和修改,根據(jù)系統(tǒng)設(shè)置對(duì)某些DNT(Do Not Translate)的頁(yè)面進(jìn)行過(guò)濾和跳轉(zhuǎn),當(dāng)然還有對(duì)Cookie的處理等等。其他問(wèn)題跟這篇文章關(guān)聯(lián)不大,我們重點(diǎn)討論在這種情況下的Cookie處理。Cookie跟隨目標(biāo)服務(wù)器的HTTP頭信息被MTS接收到,經(jīng)過(guò)MTS整理之后發(fā)給客戶端瀏覽器。MTS在接到下一次用戶對(duì)同一個(gè)站點(diǎn)的翻譯請(qǐng)求時(shí),再把從客戶端得到的Cookie發(fā)送給目標(biāo)服務(wù)器。
在以上的場(chǎng)景中,MTS充當(dāng)?shù)淖饔妙愃朴谝环NHTTP應(yīng)用代理服務(wù)器,它代替用戶取得目標(biāo)頁(yè)面,并在作出相應(yīng)處理后再提交給用戶。當(dāng)然,這種代理服務(wù)器不需要用戶修改瀏覽器的代理服務(wù)器參數(shù)或者網(wǎng)絡(luò)配置,而只是簡(jiǎn)單的在瀏覽器的地址欄中輸入一個(gè)MTS能夠識(shí)別的URL即可。此篇文章也是在這樣一個(gè)應(yīng)用場(chǎng)景的基礎(chǔ)上,展開(kāi)對(duì)HTTP應(yīng)用代理服務(wù)器如何處理Cookie的討論。
問(wèn)題的產(chǎn)生
在MTS系統(tǒng)中,目標(biāo)服務(wù)器的Cookie在兩個(gè)地方會(huì)產(chǎn)生問(wèn)題。當(dāng)MTS接收目標(biāo)服務(wù)器應(yīng)答的時(shí)候,Cookie隨著HTTP頭信息被MTS接收到的。這時(shí)候目標(biāo)服務(wù)器認(rèn)為MTS就是最終客戶,因此它賦予了Cookie與目標(biāo)服務(wù)器相符的屬性。而如果MTS把這些Cookie原封不動(dòng)的保存在HTTP頭信息中,傳給真正的最終用戶的話,用戶的瀏覽器會(huì)因?yàn)檫@些Cookie不合法而忽略它們。同理,當(dāng)Cookie從瀏覽器端傳回目標(biāo)服務(wù)器的時(shí)候,也會(huì)遇到相同的問(wèn)題。因此有必要對(duì)Cookie進(jìn)行一些處理,以保證用戶的瀏覽器能真正識(shí)別和利用這些Cookie。
但是為何用戶瀏覽器無(wú)法識(shí)別從目標(biāo)服務(wù)器傳過(guò)來(lái)的原始Cookie呢?這是因?yàn)槌鲇诎踩缘目紤],Cookie規(guī)范制定的時(shí)候?qū)ookie的產(chǎn)生和接受設(shè)置了一些嚴(yán)格的規(guī)范,不符合這些規(guī)范的Cookie,瀏覽器和服務(wù)器都將予以忽略。下面我們從Cookie規(guī)范入手進(jìn)行介紹。
Cookie的規(guī)范介紹
目前有以下幾種Cookie規(guī)范:
- Netscape cookie草案:是最早的cookie規(guī)范,基于rfc2109。盡管這個(gè)規(guī)范與rc2109有較大的差別,但是很多服務(wù)器都與之兼容。
- rfc2109, 是w3c發(fā)布的第一個(gè)官方cookie規(guī)范。理論上講,所有的服務(wù)器在處理cookie(版本1)時(shí),都要遵循此規(guī)范。遺憾的是,這個(gè)規(guī)范太嚴(yán)格了,以致很多服務(wù)器不正確的實(shí)施了該規(guī)范或仍在使用Netscape規(guī)范。
- rfc2965規(guī)范定義了cookie版本2,并說(shuō)明了cookie版本1的不足。
rfc2965規(guī)范的使用,目前并不多。rfc2109規(guī)范相應(yīng)要嚴(yán)格得多,在實(shí)際應(yīng)用上,并不是所有的瀏覽器和Web服務(wù)器都嚴(yán)格遵守。因此相比較而言,Netscape cookie草案倒是一個(gè)比較簡(jiǎn)潔和被廣泛支持的Cookie規(guī)范,因此我們?cè)谶@里以Netscape cookie草案為基礎(chǔ)進(jìn)行討論,對(duì)于其他兩種規(guī)范,我們的討論和代碼具有相同的意義。關(guān)于Netscape cookie草案的細(xì)節(jié),大家可以參照Netscape官方站點(diǎn),這里我們列舉一些和我們討論有關(guān)的內(nèi)容。
根據(jù)Netscape cookie草案的描述,Cookie 是Web 服務(wù)器向用戶的瀏覽器發(fā)送的一段ASCII碼文本。一旦收到Cookie,瀏覽器會(huì)把Cookie的信息片斷以"名/值"對(duì)(name-value pairs)的形式儲(chǔ)存保存在本地。這以后,每當(dāng)向同一個(gè)Web 服務(wù)器請(qǐng)求一個(gè)新的文檔時(shí),Web 瀏覽器都會(huì)發(fā)送之站點(diǎn)以前存儲(chǔ)在本地的Cookie。創(chuàng)建Cookie的最初目的是想讓W(xué)eb服務(wù)器能夠通過(guò)多個(gè)HTTP請(qǐng)求追蹤客戶。有些復(fù)雜的網(wǎng)絡(luò)應(yīng)用需要在不同的網(wǎng)頁(yè)之間保持一致,它們需要這種會(huì)話狀態(tài)的保持能力。
瀏覽器與Web服務(wù)器通過(guò)HTTP協(xié)議進(jìn)行通訊,而Cookie就是保存在HTTP協(xié)議的請(qǐng)求或者應(yīng)答頭部(在HTTP協(xié)議中,數(shù)據(jù)包括兩部分,一部分是頭部,由一些名值對(duì)構(gòu)成,用來(lái)描述要被傳輸數(shù)據(jù)的一些信息。一部分是主體(body),是真正的數(shù)據(jù)(如HTML頁(yè)面等))進(jìn)行傳送的。
在HTML文檔被發(fā)送之前,Web服務(wù)器通過(guò)傳送HTTP 包頭中的Set-Cookie 消息把一個(gè)cookie 發(fā)送到用戶的瀏覽器中。下面是一個(gè)遵循Netscape cookie草案的完整的Set-Cookie 頭:
Set-Cookie:customer=huangxp; path=/foo; domain=.ibm.com;
expires= Wednesday, 19-OCT-05 23:12:40 GMT; [secure]
|
Set-Cookie的每個(gè)屬性解釋如下:
- Customer=huangxp 一個(gè)"名稱=值"對(duì),把名稱customer設(shè)置為值"huangxp",這個(gè)屬性在Cookie中必須有。
- path=/foo 控制哪些訪問(wèn)能夠觸發(fā)cookie 的發(fā)送。如果沒(méi)有指定path,cookie 會(huì)在所有對(duì)此站點(diǎn)的HTTP 傳送時(shí)發(fā)送。如果path=/directory,只有訪問(wèn)/directory 下面的網(wǎng)頁(yè)時(shí),cookie才被發(fā)送。在這個(gè)例子中,用戶在訪問(wèn)目錄/foo下的內(nèi)容時(shí),瀏覽器將發(fā)送此cookie。如果指定了path,但是path與當(dāng)前訪問(wèn)的url不符,則此cookie將被忽略。
- domain=.ibm.com 指定cookie被發(fā)送到哪臺(tái)計(jì)算機(jī)上。正常情況下,cookie只被送回最初向用戶發(fā)送cookie 的計(jì)算機(jī)。在這個(gè)例子中,cookie 會(huì)被發(fā)送到任何在.ibm.com域中的主機(jī)。如果domain 被設(shè)為空,domain 就被設(shè)置為和提供cookie 的Web 服務(wù)器相同。如果domain不為空,并且它的值又和提供cookie的Web服務(wù)器域名不符,這個(gè)Cookie將被忽略。
- expires= Wednesday, 19-OCT-05 23:12:40 GMT 指定cookie 失效的時(shí)間。如果沒(méi)有指定失效時(shí)間,這個(gè)cookie 就不會(huì)被寫(xiě)入計(jì)算機(jī)的硬盤上,并且只持續(xù)到這次會(huì)話結(jié)束。
- secure 如果secure 這個(gè)詞被作為Set-Cookie 頭的一部分,那么cookie 只能通過(guò)安全通道傳輸(目前即SSL通道)。否則,瀏覽器將忽略此Cookie。
一旦瀏覽器接收了cookie,這個(gè)cookie和對(duì)遠(yuǎn)端Web服務(wù)器的連續(xù)請(qǐng)求將一起被瀏覽器發(fā)送。例如 前一個(gè)cookie 被存入瀏覽器并且瀏覽器試圖請(qǐng)求URL http://www.ibm.com/foo/index.html 時(shí),下面的HTTP 包頭就被發(fā)送到遠(yuǎn)端的Web服務(wù)器。
GET /foo/index.html HTTP/1.0
Cookie:customer=huangxp
一次典型的網(wǎng)絡(luò)瀏覽過(guò)程
在了解了Cookie協(xié)議的一些基本內(nèi)容之后,讓我們看看一次典型的網(wǎng)絡(luò)瀏覽過(guò)程中瀏覽器如何識(shí)別和處理Cookie:
- 瀏覽器對(duì)于Web服務(wù)器應(yīng)答包頭中Cookie的操作步驟:
1. 從Web服務(wù)器的應(yīng)答包頭中提取所有的cookie。
2. 解析這些cookie的組成部分(名稱,值,路徑等等)。
3. 判定主機(jī)是否允許設(shè)置這些cookie。允許的話,則把這些Cookie存儲(chǔ)在本地。
- 瀏覽器對(duì)Web服務(wù)器請(qǐng)求包頭中所有的Cookie進(jìn)行篩選的步驟:
1. 根據(jù)請(qǐng)求的URL和本地存儲(chǔ)cookie的屬性,判斷那些Cookie能被發(fā)送給Web服務(wù)器。
2. 對(duì)于多個(gè)cookie,判定發(fā)送的順序。
3. 把需要發(fā)送的Cookie加入到請(qǐng)求HTTP包頭中一起發(fā)送。
由MTS代理的網(wǎng)絡(luò)瀏覽過(guò)程
以上我們了解了在一個(gè)典型的瀏覽器與Web服務(wù)器交互的時(shí)候,Cookie的傳遞過(guò)程。下面我們將看到,如果在MTS代理網(wǎng)絡(luò)瀏覽的過(guò)程中,不對(duì)Cookie進(jìn)行修改,上面的Cookie傳遞過(guò)程將無(wú)法實(shí)現(xiàn)。
1. 假設(shè)用戶希望把http://www.ibm.com/foo/index.html頁(yè)面翻譯成法文,應(yīng)該使用如下的url對(duì)MTS發(fā)出請(qǐng)求
:
HTTP://www.mts.com/translate?url=http://www.ibm.com/foo/index.html&language=French
2. MTS接收用戶的請(qǐng)求,連接遠(yuǎn)程目標(biāo)服務(wù)器http://www.ibm.com/foo/index.html。目標(biāo)服務(wù)器做出應(yīng)答,返回HTTP頭和HTML頁(yè)面內(nèi)容。其中,典型的HTTP頭內(nèi)容如下:
HTTP/1.1 200 OK
Date: Mon, 24 Oct 2005 06:54:41 GMT
Server: IBM_HTTP_Server
Cache-Control: no-cache
Content-Length: 19885
Connection: close
Set-Cookie:customer=huangxp; path=/foo; domain=.ibm.com;
expires= Wednesday, 19-OCT-05 23:12:40 GMT
Content-Type: text/html
|
3. MTS不對(duì)Set-Cookie后的內(nèi)容作任何處理,直接把它加到用戶瀏覽器的應(yīng)答頭上發(fā)送給瀏覽器。
4. 瀏覽器將從Set-Cookie中解析出domain和path的值,分別是.ibm.com和/foo,并與請(qǐng)求的url:HTTP://www.mts.com/translate?url=http://www.ibm.com/foo/index.html&language=French進(jìn)行比較。請(qǐng)求url的domain是www.mts.com,path是/,與Set-Cookie中的屬性不符,所以瀏覽器將忽略此Cookie。
另外,在瀏覽器發(fā)送Cookie的時(shí)候也會(huì)遇到同樣的問(wèn)題,同樣如上例,如果瀏覽器里本來(lái)已經(jīng)存儲(chǔ)了http://www.ibm.com/foo/的Cookie,但由于用戶要通過(guò)MTS訪問(wèn)此站點(diǎn),瀏覽器經(jīng)不會(huì)把已經(jīng)存儲(chǔ)的Cookie上轉(zhuǎn)到MTS中,MTS也就無(wú)法把之傳遞到http://ibm.com/foo/上。
基于上面Cookie規(guī)范的介紹和例證,我們能看出,瀏覽器在接受某一個(gè)站點(diǎn)的Cookie的時(shí)候,需要檢查Cookie的參數(shù)domain、path、secure,看是否與當(dāng)前的站點(diǎn)和URL相符,如果不符的話,就會(huì)忽略。另一方面。瀏覽器在上傳Cookie的時(shí)候,也會(huì)根據(jù)當(dāng)前所訪問(wèn)站點(diǎn)的屬性,上傳相關(guān)的Cookie,而其他的Cookie則不予上傳。
至此,我們討論了需要修改Cookie的根本原因在于Cookie規(guī)范的限制。下面我們討論兩種解決問(wèn)題的思路。
解決問(wèn)題的兩種思路
Cookie的存在是要解決HTTP協(xié)議本身先天的缺陷-無(wú)狀態(tài)性,它為用戶保存了一些需要的狀態(tài)信息。因此我們解決此問(wèn)題的最本質(zhì)的出發(fā)點(diǎn),也就是找到一種途徑能為用戶保存Cookie所提供用戶狀態(tài)信息,實(shí)際上就是Name/Value對(duì)。
思路一
第一種思路就是修改目標(biāo)服務(wù)器取得的Cookie,使之符合MTS站點(diǎn)的屬性,然后作為MTS站點(diǎn)的Cookie存儲(chǔ)到用戶的瀏覽器中去。當(dāng)然,這種修改必須保留原始Cookie的所有屬性值,當(dāng)以后訪問(wèn)同一個(gè)目標(biāo)服務(wù)器的時(shí)候,MTS能根據(jù)保存的屬性值還原出原始Cookie,然后進(jìn)行提交。
具體到屬性值的保存位置,沒(méi)有太多選擇的余地,實(shí)際上,domain,path,secure,expires這幾個(gè)屬性都無(wú)法利用,只有利用name=value這一屬性對(duì)。我們的做法是創(chuàng)造一個(gè)新的Cookie,把原始Cookie的domain,path的值與name值進(jìn)行編碼,用分隔符附加在Name值的后面,符值給新的Cookie。這樣做也同時(shí)避免了不同目標(biāo)服務(wù)器如果出現(xiàn)同名的Cookie,將會(huì)互相覆蓋的情況(Cookie規(guī)范里面也規(guī)定了,客戶端以domain,path,name作為Cookie的唯一標(biāo)示)。而原始Cookie的secure和expires值,直接符給新的Cookie,新Cookie的domain和path設(shè)成缺省值,這樣,新Cookie就可以被瀏覽器正常接受。由于瀏覽器接受的所有Cookie的domain和path值都一樣,因此每次用戶對(duì)MTS提出請(qǐng)求時(shí),瀏覽器都會(huì)把所有與MTS站點(diǎn)相關(guān)的Cookie上傳,因此,MTS還需要還原原始的Cookie,過(guò)濾掉與目標(biāo)服務(wù)器不相干的Cookie,然后上傳有用的Cookie。
這種思路的優(yōu)點(diǎn)在于Cookie存儲(chǔ)在客戶端,可以做到長(zhǎng)期存儲(chǔ),瀏覽器自己根據(jù)Cookie的expires值做出判斷,省掉很多開(kāi)發(fā)的麻煩。缺點(diǎn)是轉(zhuǎn)換的過(guò)程相對(duì)較復(fù)雜。另外還有一個(gè)缺點(diǎn),也是由于Cookie規(guī)范的限制所造成的。Cookie規(guī)范對(duì)于一個(gè)瀏覽器同時(shí)能夠存儲(chǔ)的Cookie數(shù)量作出了規(guī)定。
- 總共300 個(gè)cookie
- 每個(gè)Cookie 4 K 的存儲(chǔ)容量
- 每一個(gè)domain 或者 server 20 個(gè)cookie。
以上是瀏覽器所應(yīng)達(dá)到的最小存儲(chǔ)數(shù)量,超出這個(gè)限制,瀏覽器應(yīng)該自動(dòng)按照最少最近被使用的原則刪除超出得Cookie。由于用戶有可能通過(guò)MTS這一個(gè)網(wǎng)站翻譯大量的目標(biāo)服務(wù)器,因此瀏覽器存儲(chǔ)在MTS的domain下的cookie數(shù)量就很有可能超過(guò)20個(gè),這時(shí)候就會(huì)導(dǎo)致某些Cookie被刪除。一般這也不會(huì)造成太大問(wèn)題,因?yàn)橐?guī)范是要求瀏覽器刪除最少最近被使用的Cookie,但我們?cè)趯?shí)際測(cè)試當(dāng)中發(fā)現(xiàn)有些瀏覽器并不遵守這樣的規(guī)范,而是刪除最新的Cookie,這就將導(dǎo)致用戶很大的不便。
思路二
第二種思路在于把原始的Cookie組織成dataBean,存儲(chǔ)到用戶的Session當(dāng)中去。這樣,在用戶端只需要存儲(chǔ)一個(gè)SessionID的Cookie,而不需要存儲(chǔ)所有目標(biāo)服務(wù)器的每一個(gè)Cookie。另外,當(dāng)接收到用戶的又一次翻譯請(qǐng)求時(shí),再?gòu)腟ession當(dāng)中取出所有的dataBean,逐一進(jìn)行分析,找出與用戶所請(qǐng)求的目標(biāo)服務(wù)器相符的原始Cookie,進(jìn)行提交。
這種思路可以克服上一種思路中Cookie超過(guò)標(biāo)準(zhǔn)數(shù)量時(shí)的缺陷,而且不需編碼保存原始的Cookie屬性值,減少了程序的復(fù)雜度。缺點(diǎn)是需要程序員自己處理expires。而且由于是把Cookie存儲(chǔ)在Session中,一旦Session失效,所有Cookie都將被刪除,所以,無(wú)法保存那些長(zhǎng)期的Cookie。
總之,兩種思路各有利弊,在實(shí)際應(yīng)用當(dāng)中要權(quán)衡考慮。下面我們針對(duì)兩種思路進(jìn)行技術(shù)實(shí)現(xiàn),分別對(duì)應(yīng)方案一和方案二。
由于MTS需要與目標(biāo)服務(wù)器連接,遵循HTTP協(xié)議讀取和返回Cookie,但是如果用JDK中的java.net.URLConnection處理Cookie將非常不方便,因此我們使用HTTPClient來(lái)處理與目標(biāo)服務(wù)器的連接。
方案一:Cookie存儲(chǔ)在瀏覽器端
用戶每發(fā)起一次新的請(qǐng)求,瀏覽器在檢查完本地存儲(chǔ)Cookie的有效性后,會(huì)把所有由MTS產(chǎn)生的有效Cookie附加在請(qǐng)求頭里送到MTS。MTS接受到客戶端的翻譯請(qǐng)求后,從Request中提取出所有的Cookie,還原后根據(jù)目標(biāo)服務(wù)器的domain和path進(jìn)行過(guò)濾。產(chǎn)生所有與目標(biāo)服務(wù)器相關(guān)的Cookie。
//從request中獲取所有的Cookie
javax.servlet.http.Cookie[] theCookies = request.getCookies();
ArrayList cookiesList = new ArrayList();
String url = request.getParameter("url");
String domain = URLUtil.getURLHost(url);
String path = URLUtil.getPath(url);
if (theCookies != null)
{
for (int i = 0; i < theCookies.length; i++)
{
RE r = new RE();
//用正則表達(dá)式把name項(xiàng)還原成domain,path,name
REDebugCompiler compiler = new REDebugCompiler();
r.setProgram(compiler.compile("\\|\\|"));
String[] values = r.split(theCookies[i].getName());
//"9.181.116.183||/MTModule||testCookie:value1" or " ||
||testCookie:value1"
if (values.length == 3)
{
if (values[0].trim().startsWith("."))
{
if (!domain.endsWith(values[0].trim()))
continue;
} else if (!domain.endsWith("://" + values[0].trim()))
continue;
if (!path.startsWith(values[1].trim()))
continue;
Cookie tempCookie = new Cookie();
tempCookie.setDomain(
("".equals(values[0].trim())) ? null : values[0]);
tempCookie.setPath(
("".equals(values[1].trim())) ? null : values[1]);
tempCookie.setName(
("".equals(values[2].trim())) ? null : values[2]);
tempCookie.setSecure(theCookies[i].getSecure());
tempCookie.setValue(theCookies[i].getValue());
tempCookie.setVersion(theCookies[i].getVersion());
tempCookie.setComment(theCookies[i].getComment());
cookiesList.add(tempCookie);
}
}
}
//transferedCookie用來(lái)存儲(chǔ)將被傳到目標(biāo)服務(wù)器的Cookie
Cookie[] transferedCookie = new Cookie[cookiesList.size()];
cookiesList.toArray(transferedCookie);
|
接下來(lái),需要把Cookie送到目標(biāo)服務(wù)器中。我們使用HTTPClient與目標(biāo)服務(wù)器連接。HTTPClient在與目標(biāo)服務(wù)器連接以后,允許服務(wù)器設(shè)置Cookie并在需要的時(shí)候自動(dòng)將Cookie返回服務(wù)器,也支持手工設(shè)置 Cookie后發(fā)送到服務(wù)器端。但是,由于如何處理cookie有幾個(gè)規(guī)范互相沖突:Netscape Cookie 草案、RFC2109、RFC2965,而且還有很大數(shù)量的軟件商的Cookie實(shí)現(xiàn)不遵循任何規(guī)范。 為了處理這種狀況,需要把HttpClient設(shè)置成Cookie兼容模式,這樣可以最大限度的處理好各種Cookie。下面的代碼把Cookie送到目標(biāo)服務(wù)器。
HttpClient client = new HttpClient();
//從request得到所有需要傳輸?shù)腸ookie
Cookie[] questCookie = getCookieFromRequest(request);
//設(shè)置HTTPClient為Cookie兼容模式
client.getState().setCookiePolicy(CookiePolicy.COMPATIBILITY);
if (questCookie.length > 0)
//把Cookie加到httpclient中
client.getState().addCookies(questCookie);
HttpMethod method = new GetMethod(TagerURL);
//向目標(biāo)服務(wù)器發(fā)送請(qǐng)求
int statusCode = client.executeMethod(method);
method.releaseConnection();
|
MTS把請(qǐng)求和Cookie送出后,繼續(xù)接收目標(biāo)服務(wù)器的應(yīng)答,讀取返回的原始Cookie,并轉(zhuǎn)換成可以存儲(chǔ)在用戶瀏覽器端的Cookie。下面的代碼將對(duì)原始Cookie的內(nèi)容進(jìn)行變換,保留expires和secure等項(xiàng),把domain和path項(xiàng)編碼到name中去。
//從HTTPClient中取得所有的Cookie
Cookie[] temp = client.getState().getCookies();
if (temp != null)
{
javax.servlet.httpCookie theCookie = new javax.servlet.http.Cookie[temp.length];
//逐一對(duì)Cookie進(jìn)行處理
for (int i = 0; i < temp.length; i++)
{ StringBuffer sb = new StringBuffer();
//編碼成domain||path||name
sb.append(
temp[i].getDomain() == null ? " " : temp[i].getDomain());
sb.append("||");
sb.append(temp[i].getPath() == null ? " " : temp[i].getPath());
sb.append("||");
sb.append(temp[i].getName() == null ? " " : temp[i].getName());
theCookie[i] =
new Cookie(sb.toString(),temp[i].getValue());
//復(fù)制其他項(xiàng)
theCookie[i].setMaxAge(theCookie[i].getMaxAge();
theCookie[i].setSecure(temp[i].getSecure());
theCookie[i].setVersion(temp[i].getVersion());
theCookie[i].setComment(temp[i].getComment());
}
}
|
最后一步,把這些Cookie保存到response里,隨HTTP應(yīng)答頭返回用戶瀏覽器。并保存在瀏覽器中。
//把所有轉(zhuǎn)換后的Cookie加入response
for (int i = 0; i < theCookie.length; i++) {
response.addCookie(theCookie[i]);
}
|
至此,我們已經(jīng)完成了接收用戶請(qǐng)求,轉(zhuǎn)換Cookie,發(fā)送到目標(biāo)服務(wù)器,接收目標(biāo)服務(wù)器的原始Cookie,并保存在客戶瀏覽器的整個(gè)處理過(guò)程。
方案二:Cookie存儲(chǔ)在服務(wù)器端
在此種方案中,目標(biāo)服務(wù)器返回給MTS的Cookie將被組織成dataBean,存儲(chǔ)在用戶的Session中。因此,我們首先生成一個(gè)用來(lái)存儲(chǔ)Cookie的類CookiesBean,根據(jù)它的特性,它可以繼承ArraryList類。此對(duì)象將存儲(chǔ)用戶訪問(wèn)目標(biāo)服務(wù)器時(shí)接收到的所有Cookie,并提供與新接收到的Cookie融合的功能,同時(shí)能夠刪除過(guò)期的Cookie,更新同名的Cookie。
public class CookiesBean extends ArrayList
{
/**
* 處理Cookies.
* @參數(shù) Cookies array
*/
public CookiesBean(Cookie[] cook)
{
if (cook == null)
return;
//add all cookie which isn't expired.
for (int i = 0; i < cook.length; i++)
{
if (!cook[i].isExpired())
{
add(cook[i]);
}
}
}
/**
* 融合參數(shù)中的bean
* @參數(shù) bean
* 參考: rfc2109 4.3.3 Cookie Management
*/
public void RefreshBean(CookiesBean bean)
{
if (bean == null)
return;
Iterator it = bean.iterator();
//針對(duì)bean中的每一個(gè)Cookie進(jìn)行處理
while (it.hasNext())
{
Cookie beanCookie = (Cookie) it.next();
if (beanCookie == null) continue;
ArrayList drop = new ArrayList();
Iterator thisIt = iterator();
//取出存儲(chǔ)的Cookie進(jìn)行比較和處理
while (thisIt.hasNext())
{
Cookie thisCookie = (Cookie) thisIt.next();
if (thisCookie == null) continue;
//比較name,domain和path,如果一樣的話,則把此Cookie移到drop中
if (CommonMethods
.CompString(beanCookie.getName(), thisCookie.getName())
&& CommonMethods.CompString(
beanCookie.getDomain(),
thisCookie.getDomain())
&& CommonMethods.CompString(
beanCookie.getPath(),
thisCookie.getPath()))
{
drop.add(thisCookie);
continue;
}
//刪除過(guò)期的Cookie
if (thisCookie.isExpired())
drop.add(thisCookie);
}
//刪除所有drop中的Cookie
this.removeAll(drop);
//如果beanCookie有效,則加入到存儲(chǔ)區(qū)中。
if (!beanCookie.isExpired())
add(beanCookie);
}
return;
}
}
|
當(dāng)MTS接受到客戶端的翻譯請(qǐng)求后,會(huì)從Session中提取出所有的dataBean,并得到存儲(chǔ)的所有Cookie。如以下代碼:
CookiesBean dataBean = null;
Cookie[] theCookies = new Cookie[0];
ArrayList cookiesList = new ArrayList();
//獲得Session,并獲得dataBean
HttpSession session = request.getSession(false);
if (session != null)
{
dataBean = (CookiesBean) session.getAttribute(SESSION_NAME);
}
else
{
return theCookies;
}
|
MTS在所有的存儲(chǔ)的Cookie中,檢查Cookie的Domain、path和secure的值,篩選出符合目標(biāo)服務(wù)器的Cookie。
//提取目標(biāo)服務(wù)器的domain和path
String url = context.getURL();
String domain = URLUtil.getURLHost(url);
String path = url.substring(domain.length());
String cookiedomain = null;
String cookiepath = null;
//逐個(gè)比較Cookie的domain和path
//把符合要求的Cookie紀(jì)錄到cookiesList中
for (int i = 0; i < dataBean.size(); i++)
{
Cookie cookie = (Cookie) dataBean.get(i);
if (cookie == null) continue;
cookiedomain =
(cookie.getDomain() == null) ? "" : cookie.getDomain();
cookiepath = (cookie.getPath() == null) ? " " : cookie.getPath();
if (!path.startsWith(cookiepath))
continue;
if (cookiedomain.startsWith("."))
{
if (!domain.endsWith(cookiedomain))
continue;
}
else if (!domain.endsWith("://" + cookiedomain))
continue;
if (cookie.isExpired())
continue;
if (cookie.getSecure() && url.toLowerCase().startsWith("http:"))
continue;
cookiesList.add(cookie);
}
theCookies = new Cookie[cookiesList.size()];
cookiesList.toArray(theCookies);
return theCookies;
|
把Cookie送到目標(biāo)服務(wù)器的代碼與方案一基本一樣,在此忽略。
最后一步,需要把Cookie存儲(chǔ)到Session中。下面的代碼將從目標(biāo)服務(wù)器接受Cookie,融入到dataBean中,并保存到客戶的Session中。
//從目標(biāo)服務(wù)器得到Cookie集
Cookie[] cookies = client.getState().getCookies();
CookiesBean bean = new CookiesBean(cookies);
CookiesBean dataBean = bean;
//取得用戶Session
HttpSession session = request.getSession(false);
if (session != null)
{
if (session.getAttribute(SESSION_NAME) != null)
{
//讀取Session中存取的dataBean
dataBean = (CookiesBean) session.getAttribute(SESSION_NAME);
//目標(biāo)服務(wù)器端的Cookie融合到Session中的dataBean中
dataBean.RefreshBean(bean);
}
//把最終的dataBean存入Session中
session.setAttribute(SESSION_NAME, dataBean);
}
|
至此,我們已經(jīng)完成了在Session中保存?zhèn)€目標(biāo)服務(wù)器所產(chǎn)生Cookie的整個(gè)處理過(guò)程。
關(guān)于Session的考慮
在研究完如何管理和傳遞Cookie之后,我們也需要研究一下Session的傳遞。因?yàn)槟壳按蟛糠终军c(diǎn)都在采用Session機(jī)制保存用戶狀態(tài)數(shù)據(jù),如果不能解決Session的傳遞問(wèn)題,HTTP應(yīng)用代理服務(wù)器的適用范圍同樣會(huì)大打折扣。
首先我們了解一下Session的實(shí)現(xiàn)機(jī)制。Session是一種服務(wù)器端的機(jī)制,服務(wù)器使用一種類似于散列表的結(jié)構(gòu)來(lái)保存信息。當(dāng)程序需要為某個(gè)客戶端的請(qǐng)求創(chuàng)建一個(gè)session的時(shí)候,服務(wù)器首先檢查這個(gè)客戶端的請(qǐng)求里是否已包含了一個(gè)session標(biāo)識(shí) - 稱為session id,如果已包含一個(gè)session id則說(shuō)明以前已經(jīng)為此客戶端創(chuàng)建過(guò)session,服務(wù)器就按照session id把這個(gè)session檢索出來(lái)使用(如果檢索不到,可能會(huì)新建一個(gè)),session id的值應(yīng)該是一個(gè)既不會(huì)重復(fù),又不容易被找到規(guī)律以仿造的字符串。
保存這個(gè)session id的方式之一就是采用Cookie。一般這個(gè)Cookie的名字都類似于 SESSIONID。比如WebSphere對(duì)于Web應(yīng)用程序生成的Cookie:JSESSIONID= 0001HWF4iVD94pY8Cpbx6U4CXkf:10lro0398,它的名字就是 JSESSIONID。
保存session id的其他方式還包括URL重寫(xiě)和表單隱藏字段。這兩種方式都不需要代理服務(wù)器作特殊處理。因此實(shí)際上,我們解決了Cookie的管理和傳遞的問(wèn)題之后,也就解決了Session的管理和傳遞。
結(jié)束語(yǔ)
從上面的討論中可以看出,由于Cookie本身的規(guī)范限制,HTTP應(yīng)用代理所必需面對(duì)的一個(gè)問(wèn)題就是如何對(duì)Cookie進(jìn)行正確的處理。本文對(duì)此提出了兩種解決思路并列出了實(shí)現(xiàn)代碼。對(duì)于MTS項(xiàng)目本身,我們使用的是第二種方案。開(kāi)發(fā)人員在認(rèn)識(shí)好Cookie本身的特性之后,參照本文的思路,根據(jù)自己系統(tǒng)的特點(diǎn),也會(huì)找出更適宜的解決方案。
參考資料
- Netscape Cookie Specification 對(duì)Netscape Cookie 使用的特性進(jìn)行了簡(jiǎn)要的介紹。
- RFC2965:HTTP State Management Mechanism 介紹了HTTP 狀態(tài)管理機(jī)制
- RFC2109 w3c發(fā)布的第一個(gè)官方cookie規(guī)范
- RFC2616:Hypertext Transfer Protocol 超文本傳輸協(xié)議
- Ronald Tschalr開(kāi)發(fā)了 HTTPClient,將其作為 URLConnection 的替代品。
- Jakarta Regexp Apache 的開(kāi)源項(xiàng)目,處理正則表達(dá)式的java包。