HTTP 代理如何正确处理 Cookie

简介: 大多数的 Web 应用程序都要求维护某种会话状态,如用户购物车的内容。这种会话状态的保持很多情况下需要借助于 Cookie 或者 Session 的帮助。本文结合在线页面翻译 (Machine Translation System)项目中对于 Cookie 的处理方法,探讨一下如何在 HTTP 应用代理中正确处理 Cookie 的传递和管理问题。

黄 湘平 (xphuang@cn.ibm.com), 高级软件工程师,IBM CSDL

读者定位为具有 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 头信息提交给用户。

MTS 逻辑图
MTS 逻辑图  

当然,这其中涉及到很多的应用处理。比如与各种 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; path=/foo; domain=.ibm.com; 
 expires= Wednesday, 19-OCT-05 23:12:40 GMT; [secure] 

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 头内容如下:


 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 不对 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。


 // 从 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 包。

关于作者

黄湘平是一位 IBM CSDL 的软件工程师,从 1997 年开始从事网络开发设计工作。他对 Java 的网络和 Web 开发有丰富的经验,对底层协议也有一定的了解。现在正从事企业电子商务应用系统的开发和支持。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值