| 解決問(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)行提交。 具體到屬性值的保存位置,沒有太多選擇的余地,實(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值做出判斷,省掉很多開發(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重寫和表單隱藏字段。這兩種方式都不需要代理服務(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)目本身,我們使用的是第二種方案。開發(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開發(fā)了 HTTPClient,將其作為 URLConnection 的替代品。
- Jakarta Regexp Apache 的開源項(xiàng)目,處理正則表達(dá)式的java包。
? |