Cookie与Session

初识Cookie

在web应用中,请求路径和查询字符串对业务至关重要,通过它们已经可以进行很多业务操作了,但是HTTP是一个无状态的协议,现实中的业务却是需要一定的状态的,否则无法区分用户之间的身份。如何标识和认证一个用户,最早的方案就是Cookie了。
Cookie最早是由文本浏览器Lynx合作开发者Lou Montulli在1994年网景公司开发Netscape浏览器的第一个版本时发明。它能记录服务器与客户端之间的状态,最早的用处就是用来判断用户是否是第一次访问网站。在1997年形成规范RFC 2109,目前最新的规范为RFC 6265,它是一个由浏览器和服务器共同协作实现的规范。

Cookie的处理分为如下几步:
(1)服务器向客户端发送Cookie
(2)浏览器将Cookie保存
(3)之后每次浏览器都会将Cookie发向服务器端

客户端发送的Cookie在请求报文的Cookie字段中,根据规范中的定义,Cookie值的格式是key=value;key2=value2形式的。任何请求报文中,如果Cookie值没有isVisit,都会收到“欢迎第一次访问”这样的响应。但是,如果识别到用户没有访问过我们的站点,那么我们的站点是否应该告诉客户端已经访问过的标识呢?告知客户端的方式是通过响应报文实现的,响应的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多久后过期,而不是一个具体的时间点。
HTTPOnly告知浏览器不允许通过脚本document.cookie去更改这个Cookie值,事实上,设置HttpOnly之后,这个值在document.cookie中不可见。但是在HTTP请求的过程中,依然会发送这个Cookie到服务器端。
Secrue。当Secrue值为true时,在HTTP中是无效的,在HTTPS中才有效,表示创建的Cookie只能在HTTPS连接中被浏览器传递到服务器端进行会话验证,如果是HTTP连接则不会传递该信息,所以很难被窃听到。

由于Cookie的实现机制,一旦服务器端向客户端发送了设置Cookie的意图,除非Cookie过期,否则客户端每次请求都会发送这个Cookie到服务器端,一旦设置的Cookie过多,将会导致报头较大。大多数的Cookie并不需要每次都用上,因为这会造成带宽的部分浪费。在YSlow的性能优化规则中有这么几条:
(1)减小Cookie的大小
(2)为静态组件使用不同的域名
(3)减少DNS查询
Cookie除了可以通过后端添加协议头的字段设置外,在前端浏览器中也可以通过JavaScript进行修改,浏览器将Cookie通过document.cookie暴露给了JavaScript。前端在修改Cookie之后,后续的网络请求中就会携带上修改过后的值。
目前,广告和在线统计领域是最为依赖Cookie的,通过嵌入第三方的广告或者统计脚本,将Cookie和当前页面绑定,这样就可以标识用户,得到用户的浏览行为,广告商就可以定向投放广告了。尽管这样的行为看起来很可怕,但是从Cookie的原理来说,他只能做到标识,而不能做任何具有破坏性的事情。如果依然担心自己站点的用户被记录下行为,那就不要挂任何第三方的脚本。

Session

通过Cookie,浏览器和服务器可以实现状态的记录。但是Cookie并非是完美的,前文提及的体积过大就是一个显著的问题,最为严重的问题是Cookie可以在前后端进行修改,因此数据就极容易被篡改和伪造。如果服务器端有部分逻辑是根据Cookie中的isVip字段进行判断,那么一个普通用户通过修改Cookie就可以轻松享受到VIP服务了。综上所述,Cookie对于敏感数据的保护是无效的。
为了解决Cookie敏感数据的问题,Session应运而生。Session的数据只保留在服务器端,客户端无法修改,这样数据的安全性得到一定的保障,数据也无须在协议中每次都传递。
虽然在服务器端存储数据十分方便,但是如何将每个客户和服务器中的数据一一对应起来,这里有常见的两种实现方式:

第一种:基于Cookie来实现用户和数据的映射

虽然将所有数据都放在Cookie中不可取,但是将口令放在Cookie中还是可以的。因为口令一旦被篡改,就丢失了映射关系,也无法修改服务器端存在的数据了。并且Session的有效期通常较短,普遍的设置是20分钟,如果在20分钟内客户端和服务器端没有交互产生,服务器端就将数据删除。由于数据过期时间较短,且在服务器端存储数据,因此安全性相对较高。那么口令是如何产生的呢?
一旦服务器端启用了Session,它将约定一个键值作为Session的口令,这个值可以随意约定,比如Connect默认采用connect_uid,Tomcat会采用jsessionid等。一旦服务器检查到用户请求Cookie中没有携带该值,它就会为之生成一个值,这个值是唯一且不重复的值,并设定超时时间。以下为生成Session的代码:

var sessions = {};
var key = 'session_id';

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

每个请求到来时,检查Cookie中的口令与服务器端的数据,如果过期,就重新生成,如下所示:

function(req, res) {
    var id = req.cookies[key];
    if (!id) {
        req.session = generate();
    } else {
        var 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();
        }
    }
    handle(req, res);
}       

当然仅仅重新生成Session还不足以完成整个流程,还需要在响应给客户端时设置新的值,以便下次请求时能够对应服务器端的数据。
至此,session在前后端进行对应的过程就完成了。这样的业务逻辑可以判断和设置session,以此来维护用户与服务器端的关系。这样在session中保存的数据比直接在cookie中保存数据要安全得多。这种实现方案依赖cookie实现,而且也是目前大多数web应用的方案。如果客户端禁止使用cookie,这个世界上大多数的网站将无法实现登录等操作。

第二种:通过查询字符串来实现浏览器和服务器端数据的对应

它的原理是检查请求的查询字符串,如果没有值,会先生成新的带值的URL,然后形成跳转,让客户端重新发起请求。用户访问http://localhost/pathname时,如果服务器端发现查询字符串中不带session_id参数,就会将用户跳转到http://localhost/pathname?session_id=1234567这样一个类似的地址。如果浏览器收到302状态码和Location报头,就会重新发起新的请求,这样新的请求到来时就能通过session的检查,除非内存中的数据过期。
有的服务器在客户端禁用cookie时,会采用这样方案实现退化。通过这种方案,无须在响应时设置cookie。但是这种方案带来的风险远大于基于cookie实现的风险,只要将地址栏中的地址发给另外一个人,那么他就拥有跟你相同的身份。cookie的方案在换了浏览器或者换了电脑之后无法生效,相对较为安全。