Cookie
HTTP cookie 通常也叫作 cookie,最初用于在客户端存储会话信息。这个规范要求服务器在响应 HTTP 请求时,通过发送 Set-Cookie
HTTP 头部包含会话信息。例如,下面是包含这个头部的一个 HTTP 响应:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value
这个 HTTP 响应会设置一个名为"name",值为"value"的 cookie。名和值在发送时都会经过 URL 编码。浏览器会存储这些会话信息,并在之后的每个请求中都会通过 HTTP 头部 cookie 再将它们发回服务器,比如:
GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header: other-header-value
这些发送回服务器的额外信息可用于唯一标识发送请求的客户端。
限制
cookie 只能与创建它的域一起使用,这样可以确保存储在 cookie 中的信息不会被其他域访问。
由于 cookie 存储在用户的设备上,浏览器会施加一些限制,以防止 cookie 被滥用,并且不会占用太多的存储空间。
通常,遵守以下限制可以避免在各大浏览器中遇到问题:
- 总共不超过 300 个 cookie。
- 每个 cookie 大小不超过 4096 字节。
- 每个域名下不超过 20 个 cookie。
- 每个域名下总共的 cookie 大小不超过 81920 字节。
各浏览器对每个域名的 cookie 数量有不同的限制:
- 最新版的 IE 和 Edge 限制每个域名最多 50 个 cookie。
- 最新版的 Firefox 限制每个域名最多 150 个 cookie。
- 最新版的 Opera 限制每个域名最多 180 个 cookie。
- Safari 和 Chrome 没有硬性限制。
如果 cookie 总数超过单个域名的限制,浏览器会删除一些已有的 cookie:IE 和 Opera 会删除最久未使用的 cookie(LRU 原则)。Firefox 可能会随机删除 cookie,为了避免不确定的结果,最好不要超出限制。
大多数浏览器对单个 cookie 的大小限制是 4096 字节(有时上下浮动一个字节)。为了确保跨浏览器的兼容性,最好将单个 cookie 的大小限制在 4095 字节以内。
如果创建的 cookie 超过大小限制,该 cookie 会被静默删除。需要注意,一个字符通常占用 1 字节,如果使用多字节字符(如 UTF-8),每个字符可能占用最多 4 字节。
cookie 的构成
cookie 在浏览器中是由以下参数构成的:
名称
唯一标识 cookie 的名称。cookie 名不区分大小写,因此 myCookie 和 MyCookie 是同一个名称。不过,实践中最好将 cookie 名当成区分大小写来对待,因为一些服务器软件可能这样对待它们。cookie 名必须经过 URL 编码。
值
存储在 cookie 里的字符串值。这个值必须经过 URL 编码。
域
cookie 有效的域。发送到这个域的所有请求都会包含对应的 cookie。这个值可能包含子域(如www.wrox.com),也可以不包含(如.wrox.com 表示对 wrox.com 的所有子域都有效)。如果不明确设置,则默认为设置 cookie 的域。
路径
请求 URL 中包含这个路径才会把 cookie 发送到服务器。例如,可以指定 cookie 只能由 http://www.wrox.com/books/ 访问,因此访问
http://www.wrox.com/
下的页面就不会发送 cookie,即使请求的是同一个域。
过期时间
表示何时删除 cookie 的时间戳(即什么时间之后就不发送到服务器了)。默认情况下,浏览器会话结束后会删除所有 cookie。不过,也可以设置删除 cookie 的时间。这个值是 GMT 格式(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定删除 cookie 的具体时间。这样即使关闭浏览器 cookie 也会保留在用户机器上。把过期时间设置为过去的时间会立即删除 cookie。
安全标志
设置之后,只在使用 SSL 安全连接的情况下才会把 cookie 发送到服务器。例如,请求 https://www.wrox.com 会发送 cookie,而请求 http://www.wrox.com 则不会。
这些参数在 Set-Cookie 头部中使用分号加空格隔开,比如:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.wrox.com
Other-header: other-header-value
这个头部设置一个名为"name"的 cookie,这个 cookie 在 2007 年 1 月 22 日 7:10:24 过期,对 www.wrox.com 及其他 wrox.com 的子域(如 p2p.wrox.com)有效。
安全标志 secure 是 cookie 中唯一的非名/值对,只需一个 secure 就可以了。比如:
HTTP/1.1 200 OK 17 Content-type: text/html
Set-Cookie: name=value; domain=.wrox.com; path=/; secure
Other-header: other-header-value
这里创建的 cookie 对所有 wrox.com 的子域及该域中的所有页面有效(通过 path=/指定)。不过, 这个 cookie 只能在 SSL 连接上发送,因为设置了 secure 标志。
要知道,域、路径、过期时间和 secure 标志用于告诉浏览器什么情况下应该在请求中包含 cookie。这些参数并不会随请求发送给服务器,实际发送的只有 cookie 的名/值对。
JavaScript 中的 cookie
在 JavaScript 中处理 cookie 比较麻烦,因为接口过于简单,只有 BOM 的 document.cookie
属性。根据用法不同,该属性的表现迥异。要使用该属性获取值时,document.cookie
返回包含页面中所有有效 cookie 的字符串(根据域、路径、过期时间和安全设置),以分号分隔,如下面的例子所示:
name1=value1;name2=value2;name3=value3
所有名和值都是 URL 编码的,因此必须使用 decodeURIComponent()
解码。
在设置值时,可以通过 document.cookie
属性设置新的 cookie 字符串。这个字符串在被解析后会添加到原有 cookie 中。设置 document.cookie
不会覆盖之前存在的任何 cookie,除非设置了已有的 cookie。设置 cookie 的格式如下,与 Set-Cookie 头部的格式一样:
name=value; expires=expiration_time; path=domain_path; domain=domain_name; secure
document.cookie = 'name=Nicholas';
这个 cookie 在每次客户端向服务器发送请求时都会被带上,在浏览器关闭时就会被删除。虽然这样直接设置也可以,因为不需要在名称或值中编码任何字符,但最好还是使用 encodeURIComponent()
对名称和值进行编码。
document.cookie =
encodeURIComponent('name') + '=' + encodeURIComponent('Nicholas');
要为创建的 cookie 指定额外的信息,只要像 Set-Cookie 头部一样直接在后面追加相同格式的字符串即可:
document.cookie =
encodeURIComponent('name') +
'=' +
encodeURIComponent('Nicholas') +
'; domain=.wrox.com; path=/';
因为在 JavaScript 中读写 cookie 不是很直观,所以可以通过辅助函数来简化相应的操作。与 cookie 相关的基本操作有读、写和删除。这些在 CookieUtil 对象中表示如下:
class CookieUtil {
static get(name) {
let cookieName = `${encodeURIComponent(name)}=`,
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null;
if (cookieStart > -1) {
let cookieEnd = document.cookie.indexOf(';', cookieStart);
if (cookieEnd == -1) {
cookieEnd = document.cookie.length;
}
cookieValue = decodeURIComponent(
document.cookie.substring(cookieStart + cookieName.length, cookieEnd),
);
}
return cookieValue;
}
static set(name, value, expires, path, domain, secure) {
let 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;
}
static unset(name, path, domain, secure) {
CookieUtil.set(name, '', new Date(0), path, domain, secure);
}
}
CookieUtil.get()
方法用于取得给定名称的 cookie 值。为此,需要在 document.cookie
返回的字符串中查找是否存在名称后面加上等号。如果找到了,则使用 indexOf()
再查找该位置后面的分号 (表示该 cookie 的末尾)。如果没有找到分号,说明这个 cookie 在字符串末尾,因此字符串剩余部分都是 cookie 的值。取得 cookie 值后使用 decodeURIComponent()
解码,然后返回。如果没有找到 cookie,则返回 null。
CookieUtil.set()
方法用于设置页面上的 cookie,接收多个参数:cookie 名称、cookie 值、可选的 Date 对象(表示何时删除 cookie)、可选的 URL 路径、可选的域以及可选的布尔值(表示是否添加 secure 标志)。这些参数以它们的使用频率为序,只有前两个是必需的。在方法内部,使用了 encodeURIComponent()
对名称和值进行编码,然后再依次检查其他参数。如果 expires 参数是 Date 对象,则使用 Date 对象的 toGMTString()
方法添加一个 expires 选项来获得正确的日期格式。剩下的代码就是简单地追加 cookie 字符串,最终设置给 document.cookie。
没有直接删除已有 cookie 的方法。为此,需要再次设置同名 cookie(包括相同路径、域和安全选项), 16 但要将其过期时间设置为某个过去的时间。CookieUtil.unset()
方法实现了这些处理。这个方法接收 4 个参数:要删除 cookie 的名称、可选的路径、可选的域和可选的安全标志。
这些参数会传给 CookieUtil.set()
,将 cookie 值设置为空字符串,将过期时间设置为 1970年1月1日(以 0 毫秒初始化的 Date 对象的值)。这样可以保证删除 cookie。
// 设置cookie
CookieUtil.set('name', 'Nicholas');
CookieUtil.set('book', 'Professional JavaScript');
// 读取cookie
console.log(CookieUtil.get('name')); // "Nicholas"
console.log(CookieUtil.get('book')); // "Professional JavaScript"
// 删除cookie
CookieUtil.unset('name');
CookieUtil.unset('book');
// 设置有路径、域和过期时间的cookie
CookieUtil.set(
'name',
'Nicholas',
'/books/projs/',
'www.wrox.com',
new Date('January 1, 2010'),
);
// 删除刚刚设置的cookie
CookieUtil.unset('name', '/books/projs/', 'www.wrox.com');
// 设置安全cookie
CookieUtil.set('name', 'Nicholas', null, null, null, true);
这些方法通过处理解析和 cookie 字符串构建,简化了使用 cookie 存储数据的操作。
子 cookie
为绕过浏览器对每个域 cookie 数的限制,有些开发者提出了子 cookie 的概念。子 cookie 是在单个 cookie 存储的小块数据,本质上是使用 cookie 的值在单个 cookie 中存储多个名/值对。最常用的子 cookie 模式如下:
name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5
子 cookie 的格式类似于查询字符串。这些值可以存储为单个 cookie,而不用单独存储为自己的名/ 值对。结果就是网站或 Web 应用程序能够在单域 cookie 数限制下存储更多的结构化数据。
要操作子 cookie,就需要再添加一些辅助方法。解析和序列化子 cookie 的方式不一样,且因为对子 cookie 的使用而变得更复杂。比如,要取得某个子 cookie,就需要先取得 cookie,然后在解码值之前需要先像下面这样找到子 cookie:
class SubCookieUtil {
static get(name, subName) {
let subCookies = SubCookieUtil.getAll(name);
return subCookies ? subCookies[subName] : null;
}
static getAll(name) {
let cookieName = encodeURIComponent(name) + '=',
cookieStart = document.cookie.indexOf(cookieName),
cookieValue = null,
cookieEnd,
subCookies,
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 (let 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()
方法在解析 cookie 值方面与 CookieUtil.get()
方法非常相似。不同的是 SubCookieUtil.getAll()
方法不会立即解码 cookie 的值,而是先用和号(&)拆分,将所有子 cookie 保存到数组。然后,再基于等号(=)拆分每个子 cookie,使 parts 数组的第一个元素是子 cookie 的名称,第二个元素是子 cookie 的值。两个元素都使用 decodeURIComponent()
解码,并添加到 result 对象,最后返回 result 对象。如果 cookie 不存在则返回 null。
// 假设document.cookie=data=name=Nicholas&book=Professional%20JavaScript
// 取得所有子cookie
let data = SubCookieUtil.getAll('data');
console.log(data.name); // "Nicholas"
console.log(data.book); // "Professional JavaScript"
// 取得个别子cookie
console.log(SubCookieUtil.get('data', 'name')); // "Nicholas"
console.log(SubCookieUtil.get('data', 'book')); // "Professional JavaScript"
要写入子 cookie,可以使用另外两个方法: set()
和 setAll()
。这两个方法的实现如下:
class SubCookieUtil {
// 省略之前的代码
static set(name, subName, value, expires, path, domain, secure) {
let subcookies = SubCookieUtil.getAll(name) || {};
subcookies[subName] = value;
SubCookieUtil.setAll(name, subcookies, expires, path, domain, secure);
}
static setAll(name, subcookies, expires, path, domain, secure) {
let 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 (cookieParts.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 的值、可选的 Date 对象用于设置 cookie 的过期时间、可选的 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。因为存在属性名等于空字符串的可能,所以在添加到 subcookieParts 数组之前也要检查属性名的长度。subcookieParts 数组包含了子 cookie 的名/值对,这样我们可以方便地使用 join()
方法用和号将它们拼接成字符串。剩下的逻辑与 CookieUtil.set()
一样。
// 假设document.cookie=data=name=Nicholas&book=Professional%20JavaScript
// 设置两个子cookie
SubCookieUtil.set('data', 'name', 'Nicholas');
SubCookieUtil.set('data', 'book', 'Professional JavaScript');
// 设置所有子 cookie 并传入过期时间
SubCookieUtil.setAll(
'data',
{ name: 'Nicholas', book: 'Professional JavaScript' },
new Date('January 1, 2010'),
);
// 修改"name"的值并修改整个 cookie 的过期时间
SubCookieUtil.set('data', 'name', 'Michael', new Date('February 1, 2010'));
最后一组子 cookie 相关的方法是要删除子 cookie 的。常规 cookie 可以通过直接设置过期时间为某个过去的时间删除,但删除子 cookie 没有这么简单。为了删除子 cookie,需要先取得所有子 cookie,把要删除的那个删掉,然后再把剩下的子 cookie 设置回去。下面是相关方法的实现:
class SubCookieUtil {
// 省略之前的代码
static unset(name, subName, path, domain, secure) {
let subcookies = SubCookieUtil.getAll(name);
if (subcookies) {
delete subcookies[subName]; // 删除
SubCookieUtil.setAll(name, subcookies, null, path, domain, secure);
}
}
static unsetAll(name, path, domain, secure) {
SubCookieUtil.setAll(name, null, new Date(0), path, domain, secure);
}
}
这里定义的这两个方法有两个不同的目的。unset()
方法用于从 cookie 中删除一个子 cookie,其他子 cookie 不受影响;而 unsetAll()
方法与 CookieUtil.unset()
一样,会删除整个 cookie。与 set()
和 setAll()
一样,路径、域和 secure 标志必须与创建 cookie 时使用的一样。可以像下面这样使用这两个方法:
// 只删除"name"子cookie
SubCookieUtil.unset('data', 'name');
// 删除整个cookie
SubCookieUtil.unsetAll('data');
如果实际开发中担心碰到每个域的 cookie 限制,则可以考虑使用子 cookie 这个方案。此时要特别注意 cookie 的大小,不要超过对单个 cookie 大小的限制。
使用 cookie 的注意事项
还有一种叫作 HTTP-only 的 cookie。HTTP-only 可以在浏览器设置,也可以在服务器设置,但只能在服务器上读取,这是因为 JavaScript 无法取得这种 cookie 的值。
因为所有 cookie 都会作为请求头部由浏览器发送给服务器,所以在 cookie 中保存大量信息可能会影响特定域浏览器请求的性能。保存的 cookie 越大,请求完成的时间就越长。即使浏览器对 cookie 大小有限制,最好还是尽可能只通过 cookie 保存必要信息,以避免性能问题。
对 cookie 的限制及其特性决定了 cookie 并不是存储大量数据的理想方式。因此,其他客户端存储技术出现了。
不要在 cookie 中存储重要或敏感的信息。cookie 数据不是保存在安全的环境中,因此任何人都可能获得。应该避免把信用卡号或个人地址等信息保存在 cookie 中。