解决问题的两种思路
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包。