cookie

随着Web应用程序的出现,也产生了对于能够直接在客户端上存储用户信息能力的要求。想法很合乎逻辑,属于某个特定用户的信息应该存在该用户的机器上。无论是登录信息、偏好设定或其他数据,Web应用提供者发现他们在找各种方式将数据存在客户端上。

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

HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value
Other-header: other-header-value

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

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

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

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

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

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

cookie的成分

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

名称:一个唯一确定cookie的名称。cookie名称是不区分大小写的,所以myCookie和MyCookie被认为是同一个cookie。然而,实践中最好将cookie名称看作是区分大小写的,因为某些服务离线应用与客户端存储器会这样处理cookie。cookie的名称必须是经过URL编码的。
值:储存在cookie中的字符串值。值必须被URL编码。

域:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(subdomain,如www.wrox.com​),也可以不包含它(如.wrox.com,则对于wrox.com的所有子域都有效)。如果没有明确设定,那么这个域会被认作来自设置cookie的那个域。

路径:对于指定域中的那个路径,应该向服务器发送cookie。例如,你可以指定cookie只有从​​http://www.wrox.com/books/​​​中才能访问,那么​​http://www.wrox.com​​的页面就不会发送cookie信息,即使请求都是来自同一个域的。

失效时间:表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。默认情况下,浏览器会话结束时即将所有cookie删除;不过也可以自己设置删除时间。这个值是个GMT格式的日期(Wdy,DD-Mon-YYYY HH:MM:SSGMT),用于指定应该删除cookie的准确时间。因此,cookie可在浏览器关闭后依然保存在用户的机器上。如果你设置的失效日期是个以前的时间,则cookie会被立刻删除。

安全标志:指定后,cookie只有在使用SSL连接的时候才发送到服务器。例如,cookie信息只能发送给 ​​https://www.wrox.com​​​,而​​http://www.wrox.com​​的请求则不能发送 cookie。

node中的HTTP模块会将所有的报文字段解析到 ​​req.headers​​​ 上,那么Cookie就是 ​​req.headers.cookie​​​ 。根据规范中的定义,Cookie值的格式是 ​​key=value; key2=value2​​ 形式的,如果我们需要Cookie,解析它也十分容易,如下所示:

let parseCookie = (cookie) => {
let cookies = {};
if (!cookie) {
return cookies;
}
let list = cookie.split(';');
for (let i = 0; i < list.length; i++) {
let pair = list[i].split('=');
cookies[pair[0].trim()] = pair[1];
}
return cookies;
};

function handle (req, res) {
req.cookies = parseCookie(req.headers.cookie);
// 处理请求
}

响应的Cookie值在 ​​Set-Cookie​​ 字段中。它的格式与请求中的格式不太相同,规范中对它的定义如下所示:

Set-Cookie: name=value; Path=/; Expires=Sun, 23-Apr-23 09:01:35 GMT; Domain=.domain.com;

其中 ​​name=value​​ 是必须包含的部分,其余部分皆是可选参数。这些可选参数将会影响浏览器在后续将Cookie发送给服务器端的行为。以下为主要的几个选项。

  1. ​path​​ 表示这个Cookie影响到的路径,当前访问的路径不满足该匹配时,浏览器则不发送这个Cookie。
  2. ​Expires​​​ 和 ​​Max-Age​​​ 是用来告知浏览器这个Cookie何时过期的,如果不设置该选项,在关闭浏览器时会丢失掉这个Cookie。如果设置了过期时间,浏览器将会把Cookie内容写入到磁盘中并保存,下次打开浏览器依旧有效。 ​​Expires​​​ 的值是一个UTC格式的时间字符串,告知浏览器此Cookie何时将过期, ​​Max-Age​​​ 则告知浏览器此Cookie多久后过期。前者一般而言不存在问题,但是如果服务器端的时间和客户端的时间不能匹配,这种时间设置就会存在偏差。为此, ​​Max-Age​​ 告知浏览器这条Cookie多久之后过期,而不是一个具体的时间点。
  3. ​HttpOnly​​​ 告知浏览器不允许通过脚本 ​​document.cookie​​​ 去更改这个Cookie值,事实上,设置​​HttpOnly​​​ 之后,这个值在 ​​document.cookie​​ 中不可见。但是在HTTP请求的过程中,依然会发送这个Cookie到服务器端。
  4. ​Secure​​​ 。当 ​​Secure​​​ 值为 ​​true​​ 时,在HTTP中是无效的,在HTTPS中才有效,表示创建的Cookie只能在HTTPS连接中被浏览器传递到服务器端进行会话验证,如果是HTTP连接则不会传递该信息,所以很难被窃听到。

知道Cookie在报文头中的具体格式后,下面我们将Cookie序列化成符合规范的字符串,相关代码如下:

let serialize = function (name, val, opt) {
let pairs = [`${name}=${val}`];
opt = opt || {};
if (opt.maxAge) pairs.push('Max-Age = ' + opt.maxAge);
if (opt.domain) pairs.push('Domain=' + opt.domain);
if (opt.path) pairs.push('Path=' + opt.path);
if (opt.expires) pairs.push('Expires=' + opt.expires.toUTCString());
if (opt.httpOnly) pairs.push('HttpOnly');
if (opt.secure) pairs.push('Secure');
return pairs.join('; ');
};

客户端收到这个带 ​​Set-Cookie​​​ 的响应后,在之后的请求时会在Cookie字段中带上这个值。值得注意的是, ​​Set-Cookie​​​ 是较少的,在报头中可能存在多个字段。为此 ​​res.setHeader​​ 的第二个参数可以是一个数组,如下所示:

res.setHeader('Set-Cookie', [serialize('foo', 'bar'), serialize('baz', 'val')])

Cookie的性能影响
5. 减小Cookie的大小
6. 为静态组件使用不同的域名
7. 减少DNS查询

Cookie可以在前后端进行修改,因此数据就极容易被篹改和伪造。为了解决Cookie敏感数据的问题,Session应运而生。Session的数据只保留在服务器端,客户端无法修改,这样数据的安全性得到一定的保障,数据也无须在协议中每次都被传递。

虽然在服务器端存储数据十分方便,但是如何将每个客户和服务器中的数据一一对应起来,这里有常见的基于Cookie来实现用户和数据的映射。

虽然将所有数据都放在Cookie中不可取,但是将口令放在Cookie中还是可以的。因为口令一旦被篹改,就丢失了映射关系,也无法修改服务器端存在的数据了。并且Session的有效期通常较短,普遍的设置是20分钟,如果在20分钟内客户端和服务器端没有交互产生,服务器端就将数据删除。由于数据过期时间较短,且在服务器端存储数据,因此安全性相对较高。那么口令是如何产生的呢?

一旦服务器端启用了Session,它将约定一个键值作为Session的口令,这个值可以随意约定。每个请求到来时,检查Cookie中的口令与服务器端的数据,如果过期,就重新生成,如下所示:

const http = require('http');
const crypto = require('crypto')
const queryString = require('querystring')

let parseCookie = (cookie) => {
return queryString.parse(cookie, '; ') || {}
};

// 服务器存储的cookie
let sessions = {};
let key = 'session_id';
const EXPIRES = 20 * 60 * 1000;
const SECRET = '0123456789'

let generate = function () {
let session = {};
session.id = String((new Date()).getTime() + Math.random())
session.cookie = {
expire: (new Date()).getTime() + EXPIRES
};
sessions[session.id] = session;
return session;
};

// 将值通过私钥签名,由.分割原值和签名
let sign = function (val, secret) {
return val + '.' + crypto
.createHmac('sha256', secret)
.update(val)
.digest('base64')
.replace(/\=+$/, '');
};


// 取出口令部分进行签名,对比用户提交的值
let unsign = function (val, secret) {
var str = val.slice(0, val.lastIndexOf('.'));
return sign(str, secret) == val ? str : false;
};

let server = http.createServer((req, res) => {
req.cookies = parseCookie(req.headers.cookie)
let id = req.cookies[key];
if (!id) {
// 一般会根据用户的ID来生成带签名的cookie这里只是简单的处理
req.session = generate();
res.setHeader('Set-Cookie', `${key}=${sign(req.session.id, SECRET)}`);
res.end('login')
} else {
id = unsign(id, SECRET)
let session = sessions[id];
if (session) {
if (session.cookie.expire > (new Date()).getTime()) {
// 更新超时时间
session.cookie.expire = (new Date()).getTime() + EXPIRES;
req.session = session;
} else {
// 超时了,删除旧的数据,并重新生成
delete sessions[id];
req.session = generate();
}
} else {
// 如果session过期或口令不对,重新生成session
req.session = generate();
}
res.setHeader('Set-Cookie', `${key}=${sign(req.session.id, SECRET)}`);
res.end('home')
}
})

server.listen(8080, () => {
console.log('port in 8080')
})

这里提到的Session的安全,就主要指如何让这个口令更加安全。有一种做法是将这个口令通过私钥加密进行签名,使得伪造的成本较高。客户端尽管可以伪造口令值,但是由于不知道私钥值,签名信息很难伪造。如此,我们只要在响应时将口令和签名进行对比,如果签名非法,我们将服务器端的数据立即过期即可。

cookie到底是怎样存储数据的_存储