背景

很久很久以前,Web 基本上就是文档的浏览而已, 既然是浏览,作为服务器, 不需要记录谁在某一段时间里都浏览了什么文档,每次请求都是一个新的HTTP协议, 就是请求加响应。

但是随着交互式Web应用的兴起,像在线购物网站,需要登录的网站等等,马上就面临一个问题,那就是要管理会话,必须记住哪些人登录系统, 哪些人往自己的购物车中放商品, 也就是说服务器必须把每个人区分开,这就是一个不小的挑战,因为HTTP请求是无状态的。

session

Session是一个在服务器端生成、用来存储特定用户会话所需信息的机制,代表服务器和浏览器之间的一次会话过程。
Session的详细解释如下:

  • Session是一个在单个操作人员整个操作过程中,与服务器端保持通信的唯一识别信息,它可以用来存储特定用户会话所需的信息。
  • Session是服务器端的机制,由服务器端生成,保存在服务器的内存、缓存、硬盘或数据库中。
  • Session数据有时效性,通常默认保存的时间为30分钟,可以通过配置文件修改。
  • Session是通过浏览器Cookie的sessionId的值来获取不同的用户数据。每一个Session被创建后,Cookie保存一个sessionId的值,sessionId是用来标识每个浏览器对应的存储空间的唯一标识符。
  • Session可以限制只能单向通行,可以通过session限制同一操作人员的多个请求只能单向通行。
  • Session还可以通过在session中加入token来验证同一个操作人员是否进行了并发重复的请求,在后一个请求到来时,使用session中的token验证请求中的token是否一致,当不一致时,被认为是重复提交,将不允许通过。
  • Session实际上也是利用了Cookie的技术,Cookie把键和值都存在浏览器端,而Session把sessionId存在浏览器端,而真正的数据存放在服务器端。

总之,Session是在服务器端与浏览器之间建立的一次会话过程,用于存储特定用户会话所需的信息。它通过在服务器端生成一个唯一的标识符sessionId来标识每个浏览器对应的存储空间,并把sessionId保存在浏览器的Cookie中,从而实现了在同一浏览器窗口中多个请求之间共享数据的功能。同时,Session还可以通过限制同一操作人员的多个请求只能单向通行和在session中加入token来验证请求是否合法,从而提高了系统的安全性。

解决http无状态问题 就是给大家发一个会话标识(session id), 说白了就是一个随机的字串,每个人收到的都不一样, 每次大家向服务器发起HTTP请求的时候,把这个字符串给一并捎过来, 这样服务器就能区分开谁是谁了。

session 的痛点

看起来通过 cookie + session 的方式是解决了问题, 但是我们忽略了一个问题,上述情况能正常工作是因为我们假设 server 是单机工作的,但实际在生产上,为了保障高可用,一般服务器至少需要两台机器,通过负载均衡的方式来决定到底请求该打到哪台机器上。

如图示:客户端请求后,由负载均衡器(如 Nginx)来决定到底打到哪台机器
假设登录请求打到了 A 机器,A 机器生成了 session 并在 cookie 里添加 sessionId 返回给了浏览器,那么问题来了:下次添加购物车时如果请求打到了 B 或者 C,由于 session 是在 A 机器生成的,此时的 B,C 是找不到 session 的,那么就会发生无法添加购物车的错误,就得重新登录了,此时请问该怎么办。主要有以下三种解决方案

解决方案

1. session 复制

A 生成 session 后复制到 B, C,这样每台机器都有一份 session,无论添加购物车的请求打到哪台机器,由于 session 都能找到,故不会有问题

这种方式虽然可行,但缺点也很明显:

  1. 同一样的一份 session 保存了多份,数据冗余
  2. 如果节点少还好,但如果节点多的话,特别是像阿里,微信这种由于 DAU 上亿,可能需要部署成千上万台机器,这样节点增多复制造成的性能消耗也会很大。

2. session 粘连
这种方式是让每个客户端请求只打到固定的一台机器上,比如浏览器登录请求打到 A 机器后,后续所有的添加购物车请求也都打到 A 机器上,Nginx 的 sticky 模块可以支持这种方式,支持按 ip 或 cookie 粘连等等,如按 ip 粘连方式如下

upstream tomcats {
  ip_hash;
  server 10.1.1.107:88;
  server 10.1.1.132:80;
}

3、session 共享
这种方式也是目前各大公司普遍采用的方案,将 session 保存在 redis,memcached 等中间件中,请求到来时,各个机器去这些中间件取一下 session 即可。

缺点其实也不难发现,就是每个请求都要去 redis 取一下 session,多了一次内部连接,消耗了一点性能,另外为了保证 redis 的高可用,必须做集群,当然了对于大公司来说, redis 集群基本都会部署,所以这方案可以说是大公司的首选了。

Token:no session!

通过上文分析我们知道通过在服务端共享 session 的方式可以完成用户的身份定位,但是不难发现也有一个小小的瑕疵:搞个校验机制我还得搭个 redis 集群?大厂确实 redis 用得比较普遍,但对于小厂来说可能它的业务量还未达到用 redis 的程度,所以有没有其他不用 server 存储 session 的用户身份校验机制呢,这就是我们今天要介绍的主角:token。首先请求方输入自己的用户名,密码,然后 server 据此生成 token,客户端拿到 token 后会保存到本地,之后向 server 请求时在请求头带上此 token 即可。

token介绍

Token 的中文意思是”令牌”。主要用来身份验证。比传统的身份验证方法,Token 有扩展性强,安全性高的特点,非常适合用在 Web 应用或者移动应用上。

它是服务端生成的字符串,作为客户端进行请求的一个标识。

token 疑问

1、 token 只存储在浏览器中,服务端却没有存储,这样的话我随便搞个 token 传给 server 也行?

答:server 会有一套校验机制,校验这个 token 是否合法。

2、怎么不像 session 那样根据 sessionId 找到 userid 呢,这样的话怎么知道是哪个用户?

答:token 本身可以带 uid 信息,解密后就可以获取

如何校验 token 呢?我们可以借鉴 HTTPS 的签名机制来校验。先来看 jwt token 的组成部分

可以看到 token 主要由三部分组成 1. header:指定了签名算法 2. payload:可以指定用户 id,过期时间等非敏感数据 3. Signature: 签名,server 根据 header 知道它该用哪种签名算法,再用密钥根据此签名算法对 head + payload 生成签名,这样一个 token 就生成了。当 server 收到浏览器传过来的 token 时,它会首先取出 token 中的 header + payload,根据密钥生成签名,然后再与 token 中的签名比对,如果成功则说明签名是合法的,即 token 是合法的。而且你会发现 payload 中存有我们的 userId,所以拿到 token 后直接在 payload 中就可获取 userid,避免了像 session 那样要从 redis 去取的开销

你会发现这种方式确实很妙,只要 server 保证密钥不泄露,那么生成的 token 就是安全的,因为如果伪造 token 的话在签名验证环节是无法通过的,就此即可判定 token 非法。可以看到通过这种方式有效地避免了 token 必须保存在 server 的弊端,实现了分布式存储,不过需要注意的是,token 一旦由 server 生成,它就是有效的,直到过期,无法让 token 失效,除非在 server 为 token 设立一个黑名单,在校验 token 前先过一遍此黑名单,如果在黑名单里则此 token 失效,但一旦这样做的话,那就意味着黑名单就必须保存在 server,这又回到了 session 的模式,那直接用 session 不香吗。所以一般的做法是当客户端登出要让 token 失效时,直接在本地移除 token 即可,下次登录重新生成 token 就好。

token对比session

这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:

支持跨域访问:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题
无状态:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力
更适用CDN:可以通过内容分发网络请求服务端的所有资料
更适用于移动端:当客户端是非浏览器平台时,cookie是不支持的,采用token认证方式会简单很多
无需考虑CSRF:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御

JWT介绍

JWT是JSON Web Token的缩写,它是一种开源标准(RFC 7519),用来定义通信双方如何安全地交换信息的格式。
它有很多的实现方案,比如jwt-auth,专门为php框架laravel打造,java 可以使用 java-jwt,它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。

优点
简洁: 可以通过URL、POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
自包含:负载中可以包含用户所需要的信息,避免了多次查询数据库
Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
不需要在服务端保存会话信息,特别适用于分布式微服务
缺点
无法作废已颁布的令牌
不易应对数据过期

JWT认证流程

  1. 用户使用账号和密码登录,调用后端登录接口;
  2. 后端登录程序生成jwt(注意这里小写指的是具体的token),这一步通常是由jwt插件完成的,我们只需要配置jwt加密密钥、token刷新时间、token有效时间;
  3. 后端返回jwt给前端;
  4. 前端之后的请求直接带上token即可,只要在token的有效期内;
  5. 后端收到前端的请求,会验证token的合法性、有效性,验证通过之后处理请求;
  6. 后端发送响应给前端。