| 解決問題的兩種思路 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。 //從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(); //用正則表達式把name項還原成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用來存儲將被傳到目標服務器的Cookie Cookie[] transferedCookie = new Cookie[cookiesList.size()]; cookiesList.toArray(transferedCookie); | 接下來,需要把Cookie送到目標服務器中。我們使用HTTPClient與目標服務器連接。HTTPClient在與目標服務器連接以后,允許 服務器設置Cookie并在需要的時候自動將Cookie返回服務器,也支持手工設置 Cookie后發送到服務器端。但是,由于如何處理cookie有幾個規范互相沖突:Netscape Cookie 草案、RFC2109、RFC2965,而且還有很大數量的軟件商的Cookie實現不遵循任何規范。 為了處理這種狀況,需要把HttpClient設置成Cookie兼容模式,這樣可以最大限度的處理好各種Cookie。下面的代碼把Cookie送到目 標服務器。 HttpClient client = new HttpClient(); //從request得到所有需要傳輸的cookie Cookie[] questCookie = getCookieFromRequest(request); //設置HTTPClient為Cookie兼容模式 client.getState().setCookiePolicy(CookiePolicy.COMPATIBILITY); if (questCookie.length > 0) //把Cookie加到httpclient中 client.getState().addCookies(questCookie); HttpMethod method = new GetMethod(TagerURL); //向目標服務器發送請求 int statusCode = client.executeMethod(method); method.releaseConnection(); | MTS把請求和Cookie送出后,繼續接收目標服務器的應答,讀取返回的原始Cookie,并轉換成可以存儲在用戶瀏覽器端的Cookie。下面 的代碼將對原始Cookie的內容進行變換,保留expires和secure等項,把domain和path項編碼到name中去。 //從HTTPClient中取得所有的Cookie Cookie[] temp = client.getState().getCookies(); if (temp != null) { javax.servlet.httpCookie theCookie = new javax.servlet.http.Cookie[temp.length]; //逐一對Cookie進行處理 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()); //復制其他項 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應答頭返回用戶瀏覽器。并保存在瀏覽器中。 //把所有轉換后的Cookie加入response for (int i = 0; i < theCookie.length; i++) { response.addCookie(theCookie[i]); } | 至此,我們已經完成了接收用戶請求,轉換Cookie,發送到目標服務器,接收目標服務器的原始Cookie,并保存在客戶瀏覽器的整個處理過程。 方案二:Cookie存儲在服務器端 在此種方案中,目標服務器返回給MTS的Cookie將被組織成dataBean,存儲在用戶的Session中。因此,我們首先生成一個用來存儲 Cookie的類CookiesBean,根據它的特性,它可以繼承ArraryList類。此對象將存儲用戶訪問目標服務器時接收到的所有 Cookie,并提供與新接收到的Cookie融合的功能,同時能夠刪除過期的Cookie,更新同名的Cookie。 public class CookiesBean extends ArrayList { /** * 處理Cookies. * @參數 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]); } } } /** * 融合參數中的bean * @參數 bean * 參考: rfc2109 4.3.3 Cookie Management */ public void RefreshBean(CookiesBean bean) { if (bean == null) return; Iterator it = bean.iterator(); //針對bean中的每一個Cookie進行處理 while (it.hasNext()) { Cookie beanCookie = (Cookie) it.next(); if (beanCookie == null) continue; ArrayList drop = new ArrayList(); Iterator thisIt = iterator(); //取出存儲的Cookie進行比較和處理 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; } //刪除過期的Cookie if (thisCookie.isExpired()) drop.add(thisCookie); } //刪除所有drop中的Cookie this.removeAll(drop); //如果beanCookie有效,則加入到存儲區中。 if (!beanCookie.isExpired()) add(beanCookie); } return; } } | 當MTS接受到客戶端的翻譯請求后,會從Session中提取出所有的dataBean,并得到存儲的所有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在所有的存儲的Cookie中,檢查Cookie的Domain、path和secure的值,篩選出符合目標服務器的Cookie。 //提取目標服務器的domain和path String url = context.getURL(); String domain = URLUtil.getURLHost(url); String path = url.substring(domain.length()); String cookiedomain = null; String cookiepath = null; //逐個比較Cookie的domain和path //把符合要求的Cookie紀錄到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送到目標服務器的代碼與方案一基本一樣,在此忽略。 最后一步,需要把Cookie存儲到Session中。下面的代碼將從目標服務器接受Cookie,融入到dataBean中,并保存到客戶的Session中。 //從目標服務器得到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); //目標服務器端的Cookie融合到Session中的dataBean中 dataBean.RefreshBean(bean); } //把最終的dataBean存入Session中 session.setAttribute(SESSION_NAME, dataBean); } | 至此,我們已經完成了在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包。
? |