HTTP – 服务器端 CooKie 与浏览器端 Cookie

Cookie 的来源

由于 HTTP 协议是无状态的,而服务器端的业务必须是要有状态的。Cookie 诞生的最初目的是为了存储 web 中的状态信息,以方便服务器端使用。比如判断用户是否是第一次访问网站。目前最新的规范是 RFC 6265,它是一个由浏览器服务器共同协作实现的规范。 
Cookie 的处理分为:

  • 服务器像客户端发送 cookie
  • 浏览器将 cookie 保存
  • 之后每次 http 请求浏览器都会将 cookie 发送给服务器端

服务器端的发送与解析

发送 cookie

服务器端像客户端发送 Cookie 是通过 HTTP 响应报文实现的,在 Set-Cookie 中设置需要像客户端发送的 cookie,cookie 格式如下:

Set-Cookie: “name=value;domain=.domain.com;path=/;expires=Sat, 11 Jun 2016 11:29:42 GMT;HttpOnly;secure”

其中 name=value 是必选项,其它都是可选项。Cookie 的主要构成如下:

name:一个唯一确定的cookie名称。通常来讲cookie的名称是不区分大小写的。
value:存储在cookie中的字符串值。最好为cookie的name和value进行url编码
domain:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。这个值可以包含子域(如:yq.aliyun.com),也可以不包含它(如:.aliyun.com,则对于aliyun.com的所有子域都有效).
path: 表示这个cookie影响到的路径,浏览器跟会根据这项配置,像指定域中匹配的路径发送cookie。
expires:失效时间,表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。如果不设置这个时间戳,浏览器会在页面关闭时即将删除所有cookie;不过也可以自己设置删除时间。这个值是GMT时间格式,如果客户端和服务器端时间不一致,使用expires就会存在偏差。
max-age: 与expires作用相同,用来告诉浏览器此cookie多久过期(单位是秒),而不是一个固定的时间点。正常情况下,max-age的优先级高于expires。
HttpOnly: 告知浏览器不允许通过脚本document.cookie去更改这个值,同样这个值在document.cookie中也不可见。但在http请求张仍然会携带这个cookie。注意这个值虽然在脚本中不可获取,但仍然在浏览器安装目录中以文件形式存在。这项设置通常在服务器端设置。
secure: 安全标志,指定后,只有在使用SSL链接时候才能发送到服务器,如果是http链接则不会传递该信息。就算设置了secure 属性也并不代表他人不能看到你机器本地保存的 cookie 信息,所以不要把重要信息放cookie就对了

服务器端设置 cookie 示例

var http = require('http');
var fs = require('fs');

http.createServer(function(req, res) {
    res.setHeader('status', '200 OK');
    res.setHeader('Set-Cookie', 'isVisit=true;domain=.yourdomain.com;path=/;max-age=1000');
    res.write('Hello World');
    res.end();
}).listen(8888);

console.log('running localhost:8888')

直接设置 Set-Cookie 过于原始,我们可以对 cookie 的设置过程做如下封装:

var serilize = function(name, val, options) {
    if (!name) {
        throw new Error("coolie must have name");
    }
    var enc = encodeURIComponent;
    var parts = [];

    val = (val !== null && val !== undefined) ? val.toString() : "";
    options = options || {};
    parts.push(enc(name) + "=" + enc(val));
    // domain中必须包含两个点号
    if (options.domain) {
        parts.push("domain=" + options.domain);
    }
    if (options.path) {
        parts.push("path=" + options.path);
    }
    // 如果不设置expires和max-age浏览器会在页面关闭时清空cookie
    if (options.expires) {
        parts.push("expires=" + options.expires.toGMTString());
    }
    if (options.maxAge && typeof options.maxAge === "number") {
        parts.push("max-age=" + options.maxAge);
    }
    if (options.httpOnly) {
        parts.push("HTTPOnly");
    }
    if (options.secure) {
        parts.push("secure");
    }

    return parts.join(";");
}

需要注意的是,如果给 cookie 设置一个过去的时间,浏览器会立即删除该 cookie;此外 domain 项必须有两个点,因此不能设置为 localhost:

something that wasn’t made clear to me here and totally confused me for a while was that domain names must contain at least two dots (.),hence ‘localhost’ is invalid and the browser will refuse to set the cookie!

服务器端解析 cookie

cookie 可以设置不同的域与路径,所以对于同一个 name value,在不同域不同路径下是可以重复的,浏览器会按照与当前请求 url 或页面地址最佳匹配的顺序来排定先后顺序 

所以当前端传递到服务器端的 cookie 有多个重复 name value 时,我们只需要最匹配的那个,也就是第一个。服务器端解析代码如下:

var parse = function(cstr) {
    if (!cstr) {
        return null;
    }

    var dec = decodeURIComponent;
    var cookies = {};
    var parts = cstr.split(/\s*;\s*/g);
    parts.forEach(function(p){
        var pos = p.indexOf('=');
        // name 与value存入cookie之前,必须经过编码
        var name = pos > -1 ? dec(p.substr(0, pos)) : p;
        var val = pos > -1 ? dec(p.substr(pos + 1)) : null;
        //只需要拿到最匹配的那个
        if (!cookies.hasOwnProperty(name)) {
            cookies[name] = val;
        }/* else if (!cookies[name] instanceof Array) {
            cookies[name] = [cookies[name]].push(val);
        } else {
            cookies[name].push(val);
        }*/
    });

    return cookies;
}

客户端的存取

浏览器将后台传递过来的 cookie 进行管理,并且允许开发者在 JavaScript 中使用 document.cookie 来存取 cookie。但是这个接口使用起来非常蹩脚。它会因为使用它的方式不同而表现出不同的行为。

当用来获取属性值时,document.cookie 返回当前页面可用的(根据 cookie 的域、路径、失效时间和安全设置)所有的字符串,字符串的格式如下:

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

当用来设置值的时候,document.cookie 属性可设置为一个新的 cookie 字符串。这个字符串会被解释并添加到现有的 cookie 集合中。如:

document.cookie = "_fa=aaaffffasdsf;domain=.dojotoolkit.org;path=/"

设置 document.cookie 并不会覆盖 cookie,除非设置的 name value domain path 都与一个已存在 cookie 重复。

由于 cookie 的读写非常不方便,我们可以自己封装一些函数来处理 cookie,主要是针对 cookie 的添加、修改、删除。

var cookieUtils = {
        get: function(name){
          var cookieName=encodeURIComponent(name) + "=";
          //只取得最匹配的name,value
          var cookieStart = document.cookie.indexOf(cookieName);
          var cookieValue = null;

          if (cookieStart > -1) {
            // 从cookieStart算起
            var cookieEnd = document.cookie.indexOf(';', cookieStart);
            //从=后面开始
            if (cookieEnd > -1) {
              cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, cookieEnd));
            } else {
              cookieValue = decodeURIComponent(document.cookie.substring(cookieStart + cookieName.length, document.cookie.length));
            }
          }

          return cookieValue;
        },

        set: function(name, val, options) {
            if (!name) {
                throw new Error("coolie must have name");
            }
            var enc = encodeURIComponent;
            var parts = [];

            val = (val !== null && val !== undefined) ? val.toString() : "";
            options = options || {};
            parts.push(enc(name) + "=" + enc(val));
            // domain中必须包含两个点号
            if (options.domain) {
                parts.push("domain=" + options.domain);
            }
            if (options.path) {
                parts.push("path=" + options.path);
            }
            // 如果不设置expires和max-age浏览器会在页面关闭时清空cookie
            if (options.expires) {
                parts.push("expires=" + options.expires.toGMTString());
            }
            if (options.maxAge && typeof options.maxAge === "number") {
                parts.push("max-age=" + options.maxAge);
            }
            if (options.httpOnly) {
                parts.push("HTTPOnly");
            }
            if (options.secure) {
                parts.push("secure");
            }

            document.cookie = parts.join(";");
        },
        delete: function(name, options) {
          options.expires = new Date(0);// 设置为过去日期
          this.set(name, null, options);
        }
      }