首页 » JavaScript » cookie

cookie

HTTP Cookie,通常直接叫做cookie,最初是在客户端用于存储会话信息的。该标准要求服务器对任意HTPP请求发送Set-Cookie HTTP头作为相应的一部分,其中包含会话信息。例如,这种服务器相应的头可能如下:

HTTP/1.1 200 OK
Content-Type: text/html
Set_cookie: name=value
Other-header:other-header-value

这个HTPP相应设置以name为名称、以value为值的一个cookie,名称和值在传送时必须是URL编码的。浏览器会存储这样的会话信息,并在这之后,通过为每个请求添加Cookie HTTP头将信息发送会服务器,如下所示:

GET /index.html HTTP/1.1
Cookie: name=value
Other-header: ohter-header-value

发送回服务器的额外信息可以用于唯一验证客户客户来自于发送的哪个请求。

限制

cookie在性质上是绑定在特定的域名下的。当设定了一个cookie后,在给创建它的域名发送请求时,都会包含这个cookie。这个限制确保了存储在cookie中的信息只能让批准的接受者访问,而无法被其它域访问。

由于cookie是存在客户端计算机上的,还加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间。每个域的cookie总数是有限的,不过浏览器之间各有不同。如下所示:

  • IE6以及更低版本限制每个域名最多20个cookie。
  • IE7和之后每个域名最多50个。IE7最初是支持每个域名最大20个cookie,之后被未然的一个补丁所更新。
  • Firefox限制每个域最多50个cookie。
  • Opera限制每个域名最多30个cookie。
  • Safari和Chrome对于每个域的cookie数量限制没有硬性规定。

当超过单个域名之后还要再设置cookie,浏览器就会清除以前设置的cookie。IE和Opera会删除最近最少使用的cookie以腾出空间给新设置的cookie。Firefox看上去好像是随机决定要清除那个cookie,所以考虑cookie限制非常重要,以免出现不可预期的后果。

浏览器中对于cookie的尺寸也有限制。大多数浏览器都有大约4096字节的长度限制。为了最佳的浏览器兼容性,最好将真个cookie长度限制在4095(含4095)字节以内。尺寸限制影响到一个域下所有cookie,而非每个cookie单独限制。

如果尝试创建超过最大尺寸限制的cookie,那么该cookie会被悄无声息的丢掉。注意,虽然一个字符通常占用一个字节,但是多字节情况则有所不同。

成分

cookie由浏览器保存的以下几块信息组成。

  • 名称——一个唯一确定cookie的名称。cookie名称是大小写不敏感的,所以myCookie和MyCookie被认为是同一个cookie。然而,实践中最好将cookie名称看作是大小写敏感的,因为某些服务器会这样处理cookie。cookie的名称必须是经过URL编码的。
  • 值——存储在cookie中的字符串值。值必须被URL编码。
  • 域——cookie对于那个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(如www.w3cmm.com),也可以不包含它(如.w3cmm.com,则对于w3cmm.com的所有子域都有效)。如果没有明确设定,那么这个域会被认作来自设置cookie的那个域。
  • 路径——对于指定域中那个路径,应该向服务器发送cookie。例如,你可以指定cookie只有从http://www.w3cmm/html/中才能访问,那么https://www.w3cmm.com的页面就不会发送cookie信息,即使请求都是来自同一个域的。
  • 失效时间——表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。默认情况下,浏览器会话结束时即将所有cookie删除;不过也可以自己设置删除时间。这个值是个GMT格式的日期(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定应该删除cookie的准确时间。因此,cookie可在浏览器关闭后依然保存在用户的机器上。如果你设置的失效日期是个以前的时间,则cookie被立即删除。
  • 安全标志——指定后,cookie只有在使用SSL连接的时候才发送到服务器。例如,cookie信息只能发送给https://www.w3cmm.com,而https://www.w3cmm.com的请求则不能发送cookie。

每一段信息都作为Set-Cookie头的一部分,使用分号加空格分割每一段,如下所示:

HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie:name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domin=.w3cmm.com
Other-header: other-header-value

该头信息指定了一个叫做name的cookie,它会在格林威志时间2007年1月22日 7:10:24失效,同时对于www.w3cmm.com和w3cmm.com任何子域如img.w3cmm.com都有效。

secure标志是cookie中唯一一个非名-值对的部分,直接包含一个secure单词。如下:

Content-Type: text/html
Set-Cookie: name=value; domain=.w3cmm.com; path=/; secure
Other-header: other-header-value

这里创建了一个对于所有w3cmm.com的子域和域名下(由path参数指定的)所有页面都有效的cookie。因为设置了secure标志,这个cookie只能通过SSL连接才能传输。

尤其要注意,域、路径、失效时间和secure标志都是服务器给浏览器的指示,以指定合适应该发送cookie。这些参数并不会作为发送到服务器的cookie信息的一部分,只有名-值对才会被发送。

JavaScript中的cookie

在JavaScript中处理cookie有些复杂,因为其众所周知的蹩脚的接口,即BOM的doucment.cookie属性。这个属性的独特之处在于它会因为使用它的方式不同而表现出不同的行为。当用来获取属性值时,document.cookie返回当前页面可用的(根据cookie的域、路径、失效时间和安全设置)所有cookie的字符串,一系列分号隔开的名-值对,如下例所示:

name1=value1;name2=value2;name3=value3;

所有名字和值都是经过URL编码的,所以必须使用decodeURIComponent()来解码。

当用于设置的时候,document.cookie属性可以设置为一个新的cookie字符串。这个cookie字符串会被解释并添加到现有的cookie集合中。设置document.cookie并不会覆盖cookie除非设置的cookie的名称已经存在。设置cookie的格式如下,和set-Cookie头中使用的一样的格式:

name=value; expires=expiration_time; path=domian_path; domian=domain_name; secure

这些参数中,只有cookie的名字和值是必须的。下面是一个简单的例子:

document.cookie = "name=Nicholas";

这段代码创建了一个叫name的cookie,值为Nicholas。当客户端每次向服务器端发送请求的时候,都会发送这个cookie;当浏览器关闭的时候,它就会被删除。虽然这段代码没问题,但因为这里正好名称和值都无需进行编码,所以最好每次设置cookie时都像下面这个例子中一样使用

encodeURIComponent();
document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Nicholas");

要给被创建的cookie指定额外的信息,只要将参数追加到该字符串,和Set-Cookie头中的格式一样,如下:

document.cookie = encodeURIComponent("name") + "=" + encodeURIComponent("Nicholas") + "; domain=.wrox.com; path=/";

由于JavaScript中读写cookie不是非常直观,我们常写一些函数来简化cookie的功能。基本的cookie操作有三种:读取、写入和删除。它们在CookieUtil对象中如下表示:

var CookieUtil = {
    get: function (name) {
        var cookieName = encodeURIComponent(name) + "=",
            cookieStart = document.cookie.indexOf(cookieName),
            cookieValue = null;
        if (cookieStart > -1) {
            var cookieEnd = document.cookie.indexOf(";", cookieStart)
            if (cookieEnd == -1) {
                cookieEnd = document.cookie.length;
            }
            cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd));
        }
        return cookieValue;
    },
    set: function (name, value, expires, path, domain, secure) {
        var cookieText = encodeURIComponent(name) + "=" + encodeURIComponent(value);
        if (expires instanceof Date) {
            cookieText += "; expires=" + expires.toGMTString();
        }
        if (path) {
            cookieText += "; path=" + path;
        }
        if (domain) {
            cookieText += "; domain=" + domain;
        }
        if (secure) {
            cookieText == "; secure";
        }
        document.cookie = cookieText;
    },
    unset: function (name, path, domain, secure) {
        this.set(name, "", new Date(0), path, domain, secure);
    }
};

CookieUtil.get()方法根据cookie的名字获取相应的值。他是通过在document.cookie字符串中查找cookie名加上等于号的位置。如果找到了,那么使用indexOf()查找该位置之后的第一个分号(表示了该cookie的结束位置)。如果没有找到分号,则表示该cookie是字符串中的最后一个,则域下的字符串都是cookie的值。该值使用decodeURIComponent()进行解码并最后返回。如果没有发现Cookie,则返回null。

CookieUtil.set()方法在页面上设置了一个cookie,接受几个参数:cookie的名称,cookie的值,可选的用于指定cookie何时应该被删除的Date对象,cookie的可选的URL路径,可选的域,以及可选的表示是否要添加secure标志的Boolean值。参数是按照它们使用频率的多少来排列的,只有头两个是必须的。在这个方法中,名称和值都使用encodeURIComponent()进行了URL编码,并检查其它选项。如果expires参数是Date对象,那么会使用Date对象的toGMTString()方法正确格式化Date对象,并添加到expires选项上。方法的其它部分就是构造cookie字符串并将其设置到document.cookie中。

没有删除现存cookie的直接方法。所以,需要使用相同的路径、域和安全选项再次设置cookie,并将失效时间设置为过去的时间。CookieUtil.unset()方法可以处理这种事情。它接受4个参数:要删除的cookie的名称,可选的路径参数,可选的域参数以及可选的安全参数。这些参数加上空字符串并设置失效时间为1970年1月1日(初始化为0ms的Date对象的值),传给CookieUtil.set()。这样就能确保cookie被删除。

这些方法可以如下使用:

//设置cookie
CookieUtil.set("name", "Nicholas");
CookieUtil.set("book", "Professional JavaScript");

//读值
alert(CookieUtil.get("name"));
alert(CookieUtil.get("book"));

//删除cookie
CookieUtil.unset("name");
CookieUtil.unset("book")

//设置1个cookie,包括它的路径、域、截止日期
CookieUtil.set("name", "Nicholas", "/books/projs/", "www.wrox.com", new Date("January 1, 2010"));

//删除同一cookie
CookieUtil.unset("name", "/books/projs/", "www.wrox.com");

//设置1个安全cookie
CookieUtil.set("name", "Nicholas", null, null, null, true);

这些方法通过处理解析、构造cookie字符串的任务令在客户端利用cookie存储数据更加简单。

子cookie

为了绕开浏览器的单域名下的cookie数限制,一些开发人员使用了一种称为子cookie(subcookie)的概念。子cookie是存放在单个cookie中的更小段的数据。也就是使用cookie值来存储多个名称值对儿。子cookie最常见的格式如下所示:

name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5

子cookie一般也可以查询字符串的格式进行格式化。然后这些值可以使用单个cookie进行储存和访问,而非对每个名称-值对儿使用不同的cookie存储。最后网站或者Web应用程序可以无需达到单域名cookie上限也可以存储更加结构化的数据。

为了更好地操作子cookie,必须建立一系列新的方法。子cookie的解析和序列化会因子cookie的期望用途而略有不同并更加复杂些。例如,要获得一个子cookie,首先要遵循并获得cookie一样的基本步骤,但是在解码cookie值之前,需要按如下方法找出子cookie的信息。

var SubCookieUtil = {
    get: function (name, subName) {
        var subCookies = this.getAll(name);
        if (subCookies) {
            return subCookies[subName];
        } else {
            return null;
        }
    },

    getAll: function (name) {
        var cookieName = encodeURIComponent(name) + "=",
            cookieStart = document.cookie.indexOf(cookieName),
            cookieValue = null,
            cookieEnd,
            subCookies,
            i,
            parts,
            result = {};

        if (cookieStart > -1) {
            cookieEnd = document.cookie.indexOf(";", cookieStart);
            if (cookieEnd == -1) {
                cookieEnd = document.cookie.length;
            }
            cookieValue = document.cookie.substring(cookieStart + cookieName.length, cookieEnd);

            if (cookieValue.length > 0) {
                subCookies = cookieValue.split("&");

                for (i = 0, len = subCookies.length; i < len; i++) {
                    parts = subCookies[i].split("=");
                    result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
                }

                return result;
            }
        }
        return null;
    },

//省略了更多的代码
};

获取子cookie的方法有两个:get()和getAll()。其中get()获取单个子cookie的值,getAll()获取所有子cookie并将它们放入一个对象中返回,对象的属性为子cookie的名称,对应值为子cookie对应的值。get()方法接收两个参数:cookie的名字和子cookie的名字。它其实就是调用getAll()获取所有的子cookie,然后只返回所需的那一个(如果cookie不存在则返回null)。

SubCookieUtil.getAll()方法和CookieUtil.get()在解析cookie值的方式上非常相似。区别在于cookie的值并非立即解码,而是先根据&字符将子cookie分割出来放在一个数组中,每一个子cookie再根据等于号分割,这样在parts数组中的前一部分便是子cookie名,后一部分则是子cookie的值。这两个项目都要使用decodeURIComponent()来解码,然后,放入result对象中,最后作为方法的返回值。如果cookie不存在,则返回null。

可以像下面这样使用上述方法:

//假设document.cookie=data=name=Nicholas&book=Professional%20JavaScript

//取得全部子cookie
var data = SubCookieUtil.getAll("data");
alert(data.name); //"Nicholas"
alert(data.book); //"Professional JavaScript"

//逐个取得子cookie
alert(SubCookieUtil.get("data", "name")); //"Nicholas"
alert(SubCookieUtil.get("data", "book")); //"Professional JavaScript"

要设置子cookie,也有两种方法:set()和setAll()。以下代码展示了它们的构造。

var SubCookieUtil = {
    set: function (name, subName, value, expires, path, domain, secure) {
        var subcookies = this.getAll(name) || {};
        subcookies[subName] = value;
        this.setAll(name, subcookies, expires, path, domain, secure);
    },

    setAll: function (name, subcookies, expires, path, domain, secure) {

        var cookieText = encodeURIComponent(name) + "=",
            subcookieParts = new Array(),
            subName;
        for (subName in subcookies) {
            if (subName.length > 0 && subcookies.hasOwnProperty(subName)) {
                subcookieParts.push(encodeURIComponent(subName) + "=" + encodeURIComponent(subcookies[subName]));
            }
        }

        if (subcookieParts.length > 0) {
            cookieText += subcookieParts.join("&");

            if (expires instanceof Date) {
                cookietext += "; expires=" + expires.toGMTString();
            }

            if (path) {
                cookieText += ";path=" + path;
            }

            if (domain) {
                cookieText += "; domain=" + domain;
            }

            if (secure) {
                cookieText += "; secure";
            }
        } else {
            cookieText += "; expires=" + (new Date(0).toGMTString());
        }
        document.cookie = cookieText;
    },

//省略了更多代码
};

这里的set()方法接收7个参数:cookie名称、子cookie名称、子cookie值、可选的cookie失效日期或时间的Date对象、可选的cookie路径、可选的cookie域和可选的布尔secure标志。所有的可选参数都是作用于cookie本身而非子cookie。为了在同一个cookie中存储多个子cookie,路径、域和secure标志必须一致;针对整个cookie的失效日期则可以在任何一个单独的子cookie写入的时候同时设置。在这个方法中,第一步是获取指定cookie名称对应的所有子cookie。逻辑或操作符”||”用于当getAll()返回null时将subcookies设置为一个新对象。然后,在subcookies对象上设置好子cookie值并传给setAll()。

而setAll()方法接收6个参数:cookie名称、包含所有子cookie的对象以及和set()中一样的4个可选参数。这个方法使用for-in循环遍历第二个参数中的属性。为了确保确实是要保存的数据,使用了hasOwnProperty()方法,来确保只有实例属性被序列化到子cookie中。由于可能会存在属性名为空字符串的情况,所以在把属性名加入结果对象之前还要检查一下属性名的长度。将每个子cookie的名值对儿都存入subcookieParts数组中,以便稍后可以使用join()方法以&号组合起来。剩下的方法则和CookieUtil.set()一样。

可以按如下方式使用这些方法:

//假设document.cokie=data=name=Nicholas&book=Professiona%20JavaScript

//设置两个cookie
SubCookieUtil.set("data", "name", "Nicholas");
SubCookieUtil.set("data", "book", "Professional JavaScript");

//设置全部子cookie和失效日期
SubCookieUtil.setAll("data", { name: "Nicholas", book: "Professional Javascript" }, new Data("January 1, 2010"));

//修改名字的值,并修改cookie的失效日期
SubCookieUtil.set("data", "name", "Michael", new Date("February 1, 2010"));

子cookie的最后一组方法是用于删除子cookie的。普通cokie可以通过将失效时间设置为过去的时间的方法来删除,但是子cookie不能这样做。为了删除一个子cookie,首先必须获取包含在某个cookie中的所有子cookie,然后仅删除需要删除的那个子cookie,然后再将余下的子cookie的值保存为cookie的值。请看以下代码。

var SubCookieUtil = {
    unset: function (name, subName, path, domain, secure) {
        var subcookies = this.getAll(name);
        if (subcookies) {
            delete subcookies[subName];
            this.setAll(name, subcookies, null, path, domain, secure);
        }
    },

    unsetAll: function (name, path, domain, secure) {
        this.setAll(name, null, new Date(0), path, domain, secure);
    }
};

这里定义的两个方法用于两种不同的目的。unset()方法用于删除某个cookie中单个子cookie而不影响其它的;而unsetAll()方法则等同于CookieUtil.unset(),用于删除整个cookie。和setAll()一样、域和secure标志必须和之前创建的cookie包含的内容一致。这两个方法可以像下面这样使用。

//仅删除名为name的子cookie
SubCookieUtil.unset("data", "name");

//删除整个cookie
SubCookieUtil.unsetAll("data");

如果你担心开发中可能会达到单域名的cookie上限,那么子cookie可是一个非常有吸引力的备选方案。不过,你需要更加密切关注cookie的长度,以防超过单个cookie的长度限制。

此文章发表在 JavaScript. 将 固定链接 加入收藏.