使用ETag进行session的降级

回顾

在web后台开发中我们经常需要存储一些变量到session中进行暂存,最为特殊的就是“购物车”,由于http的无状态特性,因此我们需要在客户端打上一个标记,唯一的标示客户端并和服务端session一一对应,因此就有了通过cookie和url进行存储或传递这个标示--sessionID。
sessionID是一个长的字符串,它往往默认通过cookie来保存,这个session并不持久化到硬盘而是暂存到内存中,每次请求时都会在head中带上这个包含sessionID的cookie,服务端可以根据该id标示出客户端,从而访问服务器的一片内存区;另一种通过url方式传递sessionID,当cookie在客户端被禁用时,服务端会将生成的sessionID加入到url,以此完成sessionID的传输,又称url回写。但是url回写会有明显的安全漏洞,当该网站被xss注入时,攻击者就可以通过窃取的sessionID访问服务端的隐私数据。

惊喜

无意中,在snoopyXDY的文章中看到了用ETag进行兼容,大喜,并感慨于该方法的奇妙。(之前遇到过ETag在服务器集群中同步的问题,原因是在服务端生成ETag的方式不妥,最终解决方案就是针对请求文件的内容进行hash并base64编码,这样在服务端同步的前提下,请求任意服务器都会返回相同的ETag)在此处实现中,则是异步请求一个js文件,该文件会根据客户端的相关头部设置或获取session,并且在服务端触发客户端的相关事件,完成数据的传递。
github地址:https://github.com/royalrover/ETag-Session

揭秘

首先,我们访问登陆页面login.html,在页面底部的script中,异步加载一个名称为‘eTag.js’的文件,这个文件并不是静态的,而是由服务端根据客户端传递的参数进行相应处理:如果客户端的request头部有‘if-none-match’字段,则会在内存中查看是否有该字段对应的value(服务端用hash进行存储各个客户的的session),并将该value值序列化,同时触发客户端的‘etag-ready’事件,并将序列化的value作为值传入。
在这里的实现中,有可能会存在浏览器对动态文件‘eTag.js’的缓存,为了避免‘eTag.js’的准确和实时,因此需要设置‘cache-control’头部。

router.get('/_eTag_.js',function * (next){
  var ctx = this;
  var etag = ctx.header['if-none-match'];
  var cache;

  if(!etag) {
      etag = new Date().getTime() + '__etag';
  }
//    console.log(session)
  if(session[etag]) {
    cache = session[etag];
  }else {
    cache = {
        etag: etag
    };
  }
  cache = JSON.stringify(cache);
  ctx.set('ETag',etag);
  ctx.set('cache-control','no-cache');
  ctx.set('content-type','application/javascript');
//    ctx.body = 'window._session = '+ cache + ';';
  ctx.body = 'd.do("etag-ready",'+ cache +')';
//    console.log('cache: '+cache)
  yield *next;
})

在处理post请求时,node并不会解析body,因此需要我们自己来搞定,可以通过模块,也可以简单的通过订阅事件,在这里我是简单的用node原生的request对象进行侦听。
为了避免js阻塞渲染,采用异步加载的方式获取,但这也会造成从服务端获取的数据不能及时被客户端处理和渲染,为了解决这个问题,此处采用了重量级应用必备的解决方案-Bigpipe,有服务端出发客户端订阅的事件,一旦服务端去到session数据,则触发'etag-ready'事件,并在客户端进行逻辑处理和渲染。
客户端的逻辑如下:

function $(n){
        return document.querySelectorAll(n);
    }

    function asyncLoad(src) {
        var s = document.createElement('script');
        s.src = src;
        s.async = true;
        document.body.appendChild(s);
    }

    function DO(){
        this.cbs = [];
    }
    DO.prototype.on = function(k,cb){
        if(!this.cbs[k]) {
            this.cbs[k] = [];
        }
        this.cbs[k].push(cb);
    };
    DO.prototype.do = function(k,data){
        if(!this.cbs[k])
        return;
        var cbs = this.cbs[k],len = cbs.length;
        for(var i=0;i<len;i++){
            cbs[i].call(this,data);
        }
    };

    asyncLoad('/_eTag_.js');
    var d = new DO();
    d.on('etag-ready',function(_session){
        console.log('etag-ready...');
        console.log(_session);
        if(_session && _session.etag && !_session.usrname) {
            $('[name=etag]')[0].value = _session.etag;
        }else {
            $('[name=usrname]')[0].value = _session.usrname;
            $('[name=pwd]')[0].value = _session.pwd;
            $('[name=etag]')[0].value = _session.etag;
        }
    })

总结

使用ETag方式来hack兼容性是非常棒的,几乎所有的服务器都实现了这个机制(HTTP1.1规范),因此兼容性不是问题。由于使用ETag加载的文件的元数据都保存在浏览器的缓存中,因此安全性是没法与存储在内存中的cookie方式相比的,而且如果清空浏览器缓存,那么客户端则丢失sessionID,没法在使用session。因此这种方式也仅仅作为cookie被禁用的一种候补方案,不推荐大规模使用。

posted @ 2015-07-06 20:18  royalrover  阅读(1118)  评论(0编辑  收藏  举报