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规范:

  1. Netscape cookie草案:是最早的cookie规范,基于rfc2109。尽管这个规范与rc2109有较大的差别,但是很多服务器都与之兼容。
  2. rfc2109, 是w3c发布的第一个官方cookie规范。理论上讲,所有的服务器在处理cookie(版本1)时,都要遵循此规范。遗憾的是,这个规范太严格了,以致很多服务器不正确的实施了该规范或仍在使用Netscape规范。
  3. 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的每个属性解释如下:

  1. Customer=huangxp
    一个"名称=值"对,把名称customer设置为值"huangxp",这个属性在Cookie中必须有。
  2. path=/foo
    控制哪些访问能够触发cookie 的发送。如果没有指定path,cookie 会在所有对此站点的HTTP 传送时发送。如果path=/directory,只有访问/directory 下面的网页时,cookie才被发送。在这个例子中,用户在访问目录/foo下的内容时,浏览器将发送此cookie。如果指定了path,但是path 与当前访问的url不符,则此cookie将被忽略。
  3. domain=.ibm.com
    指定cookie被发送到哪台计算机上。正常情况下,cookie只被送回最初向用户发送cookie 的计算机。在这个例子中,cookie 会被发送到任何在.ibm.com域中的主机。如果domain 被设为空,domain 就被设置为和提供cookie 的Web 服务器相同。如果domain不为空,并且它的值又和提供cookie的Web服务器域名不符,这个Cookie将被忽略。
  4. expires= Wednesday, 19-OCT-05 23:12:40 GMT
    指定cookie 失效的时间。如果没有指定失效时间,这个cookie 就不会被写入计算机的硬盘上,并且只持续到这次会话结束。
  5. 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包头中一起发送。

有代理的网络浏览过程

下面我们将看到,如果在代理服务器代理网络浏览的过程中,不对Cookie进行修改,上面的Cookie传递过程将无法实现。

  1. 假设用户希望把 http://www.ibm.com/foo/index.html 页面翻译成法文,应该使用如下的url对代理服务器发出请求:http://www.mts.com/translate?url=http://www.ibm.com/foo/index.html&language=French
  2. 代理服务器接收用户的请求,连接远程目标服务器 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
  1. 代理服务器不对Set-Cookie后的内容作任何处理,直接把它加到用户浏览器的应答头上发送给浏览器。
  2. 浏览器将从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 的管理和传递。

参考文章点这里

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐