cookie机制
认识 Cookie
cookie 是以 K-V 形式,存储在浏览器中一种数据。它可以在服务端设置,也可以在浏览器端用 js 代码设置。它拥有 maxAge、domain、path 等属性,借助这些属性,可以实现父子域名之间的数据传递。
设置 Cookie
虽然 cookie 是 K-V 形式存储的,但是在设置 cookie 的值的时候,是直接给定形如key1=value1; key2=value2
的字符串。它在服务器/浏览器端均可以设置:
- 浏览器端:通过 js 代码来设置,例如
document.cookie = "firstName=dongyuanxin; path=/
- 服务器端:通过给 Http Response Headers 中的
Set-Cookie
字段赋值,来设置 cookie。客户端接收到Set-Cookie
字段后,将其存储在浏览器中。
在服务端,以 koajs 为例,设置 key 为id
,value 为xxoo521.com
的 cookie。代码如下:
1 | const Koa = require("koa"); |
代码调试
在启动上面代码,并且请求/api
接口后。在 Chrome Dev Tools 中,能看到服务器返回的 Headers
的信息,如下图:
按照协议,浏览器应该成功保存了 cookies 的值。此时,找到Application => Storage => Cookies => 当前域名
,即可验证 cookie
是否设置成功,如下图:
整体流程
以用户购买商品为例,整体流程如下:
- 监测到浏览器客户端没有标识用户的 cookie,跳转到登陆界面
- 用户账号密码登陆,后端验证,成功后,在
Set-cookie
中设置标识用户的 cookie - 登陆成功,保存用户标识的 cookie
- 购买商品,自动携带用户身份的 cookie,后端验证无误后,购买成功
总结
由此可见,单纯的使用 cookie,需要将用户的身份信息保存在客户端,并不安全。除此之外,cookie 还有大小限制,以及只能使用字符串类型作为 value 值。
Session机制
认识 Session
Session 机制准确来说,也是通过 K-V 数据格式来保存状态。其中:
- Key:也称 SessionID,保存在客户端浏览器。
- Value:也称“Session”,保存在服务端。
可以看到,客户端只需要存储 SessionID。具体映射的数据结构放在了服务端,因此跳出了仅仅浏览器 cookie 只可以存储 string 类型的限制。
而客户端存储 SessionID,还是需要借助 cookie 来实现。
整体流程
假设/login
接口登陆成功后,服务器可以生成 sessionId 和 session。其中,session 中保存了过期时间,一些冗余信息等。代码如下:
1 | router.get("/login", async (ctx, next) => { |
然后客户端在 cookies 中携带 sessionId,访问/userInfo
接口,获得用户信息。服务端检查 sessionId 合法性,以及是否过期。代码如下:
1 | router.get("/userInfo", async (ctx, next) => { |
打开 chrome dev tools,我们可以看到,http 请求自动携带了 cookie 中的
sessionId。并且通过后端检验,拿到了用户信息。
总结:比较 cookie 与 session
session 传输数据少,数据结构灵活:相较于 cookie 来说,session 存储在服务端,客户端仅保留换取 session 的用户凭证。因此传输数据量小,速度快。
session 更安全:检验、生成、验证都是在服务端按照指定规则完成,而 cookie 可能被客户端通过 js 代码篡改。
session 的不足:服务器是有状态的。多台后端服务器无法共享 session。解决方法是,专门准备一台 session 服务器,关于 session 的所有操作都交给它来调用。而服务器之间的调用,可以走内网 ip,走 RPC 调用(不走 http)。
Token机制
为什么需要 Token?
这也是我刚接触 token 时候的疑惑,因为 session 对比 cookie 来说,解决了很多问题,所以感觉上 session 已经很完美了。
但对于 session 来说,服务器是有状态的。这个事情就很麻烦,尤其是在分布式部署服务的时候,需要共享服务器之间的状态。总不能让用户不停重复登陆吧?虽然专门准备一个服务器用来处理状态是可行的,但是能不能让服务器变成无状态的,还不能像单纯 cookie 那么蹩脚?
token 就解决了这个问题。它将状态保存在客户端,并且借助加密算法进行验证保证安全性。
整体流程
如上图所示,整体流程总结如下:
- 用户尝试登陆
- 登陆成功后,后端依靠加密算法,将凭证生成 token,返回给客户端
- 客户端保存 token,每次发送请求时,携带 token
- 后端再接收到带有 token 的请求时,验证 token 的有效性
在整个流程中,比较重要的是:生成 token、验证 token 的过程。这里设计一种简单的技术实现:
- 生成:token 的组成为:
${user}.${HS256(user, secret)}
。其中,secret 是加密需要的密钥,保存在服务端,不能泄漏。HS256 是加密算法,使用 RS256、HS512 也可以。 - 验证:将请求中携带的 token 按照
.
分开,得到payload
和sig
。用服务器密钥对payload
进行加密,将加密结果和sig
比较,如果相同,那么通过验证。
值得一提的是,这里无需对sig
进行解密。
代码实现
借助crypto
实现 HS256 算法加密:
1 | /** |
用户登陆成功后,将用户名作为payload
,生成对应的sig
,拼接为 token,返回给客户端:
1 | router.get("/login", async (ctx, next) => { |
客户端再请求需要用户身份的 api 的时候,应该携带 token,服务端对 token 合法性进行检验:
1 | router.get("/userInfo", async (ctx, next) => { |
请求成功后,服务端返回数据,下图所示:
总结:token 真香
token 的优点多多:
- 服务器变成无状态了,实现分布式 web 应用授权
- 可以进行跨域授权,不再局限父子域名
- token 设计绝对了它本身可以携带更多不敏感数据,例如最常用的 JWT
- 安全性更高,密钥保存在服务器。若密钥被窃取,可以统一重新下发密钥。
当然,token 增加了服务器压力(毕竟要加密)。但还是真香