一文讲清http代理的cookie问题
http使用cookie
HTTP请求,Cookie的使用过程
1、server通过HTTP Response中的"Set-Cookie: header"把cookie发送给client
2、client把cookie通过HTTP Request 中的“Cookie: header”发送给server
3、每次HTTP请求,Cookie都会被发送。
http请求发送cookies的条件:
1、本地已经缓存有cookies
2、根据请求的URL来匹配cookies的domain、path属性,如果都符合才会发送
举个例子:访问www.baidu.com时,就不发送www.qq.com的cookies.
cookie作用
Cookie是用于维持服务端会话状态的,通常由服务端写入,在后续请求中,供服务端读取。
cookie需要特殊处理的原因
目标服务器的Cookie在两个地方会产生问题。当代理服务器接收目标服务器应答的时候,Cookie随着HTTP头信息被代理服务器接收 到的。这时候目标服务器认为代理服务器就是最终客户,因此它赋予了Cookie与目标服务器相符的属性。而如果代理服务器把这些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草案的描述,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的操作步骤:
- 从Web服务器的应答包头中提取所有的cookie。
- 解析这些cookie的组成部分(名称,值,路径等等)。
- 判定主机是否允许设置这些cookie。允许的话,则把这些Cookie存储在本地。
浏览器对Web服务器请求包头中所有的Cookie进行筛选的步骤:
- 根据请求的URL和本地存储cookie的属性,判断哪些Cookie能被发送给Web服务器。
- 对于多个cookie,判定发送的顺序。
- 把需要发送的Cookie加入到请求HTTP包头中一起发送。
有代理的网络浏览过程
下面我们将看到,如果在代理服务器代理网络浏览的过程中,不对Cookie进行修改,上面的Cookie传递过程将无法实现。
- 假设用户希望把 http://www.ibm.com/foo/index.html 页面翻译成法文,应该使用如下的url对代理服务器发出请求:http://www.mts.com/translate?url=http://www.ibm.com/foo/index.html&language=French
- 代理服务器接收用户的请求,连接远程目标服务器 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
- 代理服务器不对Set-Cookie后的内容作任何处理,直接把它加到用户浏览器的应答头上发送给浏览器。
- 浏览器将从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,但由于用户要通过代理服务器访问此站点,浏览器经不会把已经存储的Cookie上传到代理服务器中,代理服务器也就无法把之传递到http://ibm.com/foo/上。
基于上面Cookie规范的介绍和例证,我们能看出,浏览器在接受某一个站点的Cookie的时候,需要检查Cookie的参数domain、 path、secure,看是否与当前的站点和URL相符,如果不符的话,就会忽略。另一方面。浏览器在上传Cookie的时候,也会根据当前所访问站点 的属性,上传相关的Cookie,而其他的Cookie则不予上传。
cookie处理的方法
第一种方法
第一种思路就是修改目标服务器取得的 Cookie,使之符合代理服务器的属性,然后作为代理服务器的 Cookie存储到用户的浏览器中去。当然,这种修改必须保留原始Cookie 的所有属性值,当以后访问同一个目标服务器的时候,代理服务器能根据保存的属性值还原出原始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 值都一样,因此每次用户对代理服务器提出请求时,浏览器都会把所有与代理服务器相关的 Cookie 上传,因此,代理服务器还需要还原原始的 Cookie,过滤掉与目标服务器不相干的Cookie,然后上传有用的Cookie。
这种思路的优点在于 Cookie 存储在客户端,可以做到长期存储,浏览器自己根据 Cookie 的 expires 值做出判断,省掉很多开发的麻烦。缺点是转换的过程相对较复杂。另外还有一个缺点,也是由于 Cookie 规范的限制所造成的。Cookie 规范对于一个浏览器同时能够存储的 Cookie数量作出了规定。
总共 300 个 cookie
每个 Cookie 4 K 的存储容量
每一个 domain 或者 server 20 个 cookie。
以上是浏览器所应达到的最小存储数量,超出这个限制,浏览器应该自动按照最少最近被使用的原则删除超出得 Cookie。由于用户有可能通过代理服务器访问的目标服务器,因此浏览器存储在代理服务器的 domain 下的 cookie 数量就很有可能超过 20 个,这时候就会导致某些 Cookie 被删除。一般这也不会造成太大问题,因为规范是要求浏览器删除最少最近被使用的 Cookie,但我们在实际测试当中发现有些浏览器并不遵守这样的规范,而是删除最新的 Cookie,这就将导致用户很大的不便。
第二种方法
第二种思路在于把原始的 Cookie 组织成 dataBean,存储到用户的 Session 当中去。这样,在用户端只需要存储一个 SessionID 的 Cookie,而不需要存储所有目标服务器的每一个 Cookie。另外,当接收到用户的又一次翻译请求时,再从 Session 当中取出所有的 dataBean,逐一进行分析,找出与用户所请求的目标服务器相符的原始 Cookie,进行提交。
这种思路可以克服上一种思路中 Cookie 超过标准数量时的缺陷,而且不需编码保存原始的 Cookie 属性值,减少了程序的复杂度。缺点是需要程序员自己处理 expires。而且由于是把 Cookie 存储在 Session 中,一旦 Session 失效,所有 Cookie 都将被删除,所以,无法保存那些长期的 Cookie。
总之,两种思路各有利弊,在实际应用当中要权衡考虑。
实现代码
方案一:Cookie 存储在浏览器端
用户每发起一次新的请求,浏览器在检查完本地存储 Cookie 的有效性后,会把所有由代理服务器产生的有效 Cookie 附加在请求头里送到代理服务器。代理服务器接受到客户端的翻译请求后,从 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();
代理服务器把请求和 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 将被组织成 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;
}
}
当代理服务器接受到客户端的请求后,会从 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;
}
代理服务器在所有的存储的 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 送到目标服务器的代码与方案一基本一样
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();
最后一步,需要把 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 的管理和传递。
参考文章点这里
更多推荐

所有评论(0)