[date: 2018-08-21 17:04] [visits: 8]

session、token与jwt

自认为对session、token与jwt理解还可以,这次来讲讲这个话题。

session与cookie

什么是session

session翻译过来是会话,但在WEB领域常常是指会话数据:“同一客户端与服务端进行沟通时的上下文信息”,session的作用,我用一个例子来做比喻:

假设我在滴滴平台上叫车被司机拒载后,拨通了滴滴客服电话进行投诉,通话开始,我是客户端,滴滴客服则是服务端,我每一次讲话就是发送一个请求,她每一次回复则是响应

以下是一种可能的沟通情形:

我:你好,我在你们平台上叫了一个车被司机拒载,我现在想投诉他。
客服MM:您好,给您带来不便非常抱歉,请问您的滴滴账号是多少?
我:185xxxx8888。
客服MM:您好,请问您要投诉哪一个行程订单?
我:今天下午3点,从南山星巴克到北山星巴克,司机车牌尾号xxx。
...

在我与滴滴客服的这次沟通中,session数据就是沟通过程中的信息上下文(滴滴账号、行程信息、司机车牌...),它的作用不言而喻,没有这些信息客服就不知道谁因为什么在投诉谁,所以session在这的作用是能帮助我完成这个投诉。

通过例子,我们能对session有一个简单理解,但对于HTTP应该如何记住session的内容却又是另外一回事,也许这个案例中客服接通我的电话时便撕出了一张便签纸,便签纸的页码是ignore-sb,随后过程中我与她沟通的信息她便记录在这张便签纸上。

在一对一的客服例子中,客服一直知道是与“我”在对话,但针对HTTP的情况是:“一个客服(服务端)同时处理成百上千的投诉(客户端)”,客服面临的挑战是在串线的情况下,将投诉过程中每一个有效信息记录到对应的便签纸上。由于串线,客服无法确定与她沟通的是哪个投诉用户,也就不能将用户发言中的有效信息记录到对应的便签纸上,在技术上这称为HTTP协议的无状态性:“没有来电显示的多路复用通讯”。

针对HTTP通信,如果同一个客户端先后发出两个请求,在服务端,如果不借用一些特殊手段是无法得知这两个请求来自同一个客户端,这就是HTTP协议的无状态性,它导致无法追踪会话,cookie的出现就是为了解决这个问题。

cookie是HTTP协议中定义的一个请求头部字段,浏览器针对这个头部字段会有一些额外处理逻辑,如自动携带、跨域检查与过期删除等,与之对应的响应头部是set-cookie,用于服务器向客户端写入cookie数据。

session与cookie的关系

我们需要克服HTTP协议的无状态性,让服务端能够识别同一客户端,这样才能记录并利用session信息,针对上述例子,滴滴公司为此制订了一个投诉规则:

投诉用户如果知道自己的便签ID,则在每一次发言中必须先说:“我的便签是xxx”(cookie),然后再讲述内容,而客服则判断用户有没有讲出自己的便签ID,有则在已存在中寻找,无则从便签本中撕取,如果是撕取需要在回答的时候告知(set-cookie)用户这次撕取便签的ID,投诉用户会记住该ID并在后续的每次发言中优先说出:“我的便签是xxx”

如果大家严格遵守该规则,客服针对所有的用户发言都能够找到相应的便签并查看或更新便签上的信息,这样客服在串线情况下也能识别用户(假设串线不会打断用户的单次发言)。

在HTTP通信中,客户端请求在特定位置讲出服务端曾经给予自己的某些信息,浏览器的这个特定位置就是cookie,这是浏览器中最常见的做法。

cookie只是一个特定位置,但session并不依赖这个特定位置,条件允许的情况下我们可以更换成另外一个位置,从而我们知道session并不依赖cookie,如果激进一点,甚至可以说:“session与cookie没有关系”。

session也许可以脱离cookie存在,但cookie是为session而生,多年前网景公司为了解决HTTP无状态的问题而引进cookie这个HTTP头部,就是为了定义浏览器行业传输session的标准,现在所有的浏览器都支持cookie的一系列特性,如自动携带、跨域检查与过期删除等。不过随着HTTP应用场景的发展,现今很多HTTP场景也都不再受限于浏览器内,所以认识session与cookie的关系就很有必要,当处于在非浏览器环境下我们可以选择扩展HTTP头部等方式携带session信息。

cookie追踪session的技术实现

这一小节,我用Node.js实现了一个最简单的cookie/session的example。

session与cookie主要涉及两个HTTP头部:set-cookie与cookie,前者是一个响应头部,用来告知客户端需要记住的信息,后者是一个请求头部,用来告知服务端自己曾经记住的信息。

Talk is cheap show you the code:

// index.js
const http = require('http');
const _ = require('lodash');

const session = {};

const server = http.createServer((req, res) => {
    let cookie = {};

    // 从req.headers上获取cookie并解析为对象
    _.each((req.headers.cookie || '').split(';'), item => {
        let [name, value] = item.split('=');

        cookie[name] = value;
    });

    // 通过cookie找到session数据,这里使用's'作为sessionId的在cookie中的名称
    if (cookie.s) {
        req.session = session[cookie.s] || {};
    }

    // 用户首次访问(没有sessionId或者session是空)
    // 设置cookie并初始化session
    if (!cookie.s || _.isEmpty(req.session)) {
        req.session = {
            firstVisit: Date.now()
        };

        let sessionId = Math.random().toString(16).substr(2);
        session[sessionId] = req.session;

        // 通过`set-cookie`向客户端设置cookie
        // 此处没有考虑多个cookie情况
        res.setHeader('set-cookie', `s=${sessionId}`);
    }

    res.end(`your first visit was ${req.session.firstVisit}.`);
});

server.listen(10000);

通过node index.js运行server,在浏览器中访问http://localhost:10000多次,每次显示的信息都是相同的,这就是因为根据cookie识别了同一用户,取到的session数据是相同的。

上面代码由于把session放在内存中,故不能支持水平扩展,进程重启也会导致所有session丢失,解决这两个问题的一般选择是将session数据存储在Redis中。

token

什么是token

token,翻译过来是令牌的意思,一般用于请求需要权限校验的接口,提供一个有效token就允许访问,否则拒绝访问。

token,正常是通过身份认证而获取的一个字符串凭证,这个凭证可用于在一定时间内请求一些权限需要才能获取的资源,其中最常见的认证方式是用户名+密码。

token相比每次认证的优点

因为认证的方式多种多样,进行身份验证后发放token作为授权,相比每次都进行身份验证显得更为灵活。比如手机号+验证码认证与证书认证所需成本相对较大,难以在每一个需要身份校验的地方都进行这种认证,所以使用token就显得非常有必要。另外,通过认证发放token的方式能相对减少原始认证要素被盗的几率,同时认证所发放的token一般具有有效期(越敏感有效期越短),也能降低被盗后的可能损失。

token与session及cookie的关系

token之所以会跟session与cookie扯上关系,需要从用户场景进行解释,一个web服务提供者面对的用户通常分为两种:游客与登录用户,对于游客我们一般不存储session,而登录用户至少需要记住其登录状态。对于如何记住用户登录状态,常见做法是在用户登录时,在其对应session中记录用户ID,而对于需要登录权限的操作,我们可以检查用户的session中是否存在用户ID,否则拒绝操作。

上述这种借助session识别用户是否登录的方式,客户端cookie中用于追踪session的那个值(sessionId)可被称为token,这个token通常是用户名+密码认证成功后所得到的。由此可见session与cookie跟token产生联系,是因为token的概念恰好有一个用session+cookie实现的场景。

token就是token,只是在特定场景利与session、cookie产生了联系,因此我们完全可以选择不借助session与cookie而使用token,比如在HTTP请求头中使用x-token头部传输token的值,又或者是在URL中携带token,这两者在如今都是较常见的做法。

jwt

什么是jwt

json形式的网站令牌,英文全称json web token,其实与token没多大区别,无非是使用json格式传输token罢了。

jwt跟session-token的区别

jwt是token的一种,但它与传统session-token的核心区别并不是因为jwt使用json格式,而是在使用方式上有所区别。前面讲到token与session及cookie的关系时,sessionId就是token,而sessionId对应的session数据保存在服务器端,一般需要持久化才能满足应用水平扩展需要。

当用户量足够大的时候,考虑到session数据的存储是单点,可能遭遇瓶颈,于是jwt出现了,它的最大优势就是可以消灭session存储这个单点。jwt的具体做法并不复杂,只是把之前存储在服务端的session数据用json格式转移到客户端存储,同时为了不被破解和窜改会使用加密和签名两个手段。

上述描述过程中我将session与jwt所携带的数据等同了,严格意义上他们并不等同,因为jwt携带的数据更多的是关注与授权验证有关的,而所有与会话相关的数据都可以称为session,比如我认为cookie中的所有数据都可以算是session的一部分。

总结

综合前文,这里再对token、jwt、session、cookie做一个简述:

彩蛋

过了几天,在app内看到滴滴的投诉处理完成了,故打电话向客服询问处理结果:

我:你好,我在app内看到上次的投诉处理完成了,请问处理结果是啥?
客服MM:您好,您的投诉我们相关部门已经处理,但是处理结果不对外公布。
我:处理结果不对外公布,那我如何知道你们已经处理?
客服MM:先生,您的投诉我们相关部门已经处理,但是无法告知您。
我:...
客服MM:先生,处理结果是我们相关部门已经处理。
我:...
客服MM:先生,您好,我们公司规定处理结果不对外公布。
我:...
客服MM:先生,真的非常抱歉,我们已经处理,但是处理结果不对外公布。
...

这是一个真实的故事,派单给司机时离我大概600米,可能不想跑了就打电话找借口让我取消,我没有答应,随后,司机把车从我眼前开走并在APP内操作已到达,5分钟后算我超时被取消,气炸了后投诉司机,最终也没能知道有没有处理以及怎么处理,这种投诉结果不对外公布是谁TM想出来的?