本文你将看到:
基于 HTTP 的前端鉴权背景
cookie 为什么是最方便的存储方案,有哪些操作 cookie 的方式
session 方案是如何实现的,存在哪些问题
token 方案是如何实现的,如何进行编码和防篡改?jwt 是做什么的?refresh token 的实现和意义
session 和 token 有什么异同和优缺点
单点登录是什么?实现思路和在浏览器下的处理
从状态说起
「HTTP 无状态」我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。「标记」那解决办法是什么呢?::标记::。
在学校或公司,入学入职那一天起,会录入你的身份、账户信息,然后给你发个卡,今后在园区内,你的门禁、打卡、消费都只需要刷这张卡。
「前端存储」 这就涉及到一发、一存、一带,发好办,登陆接口直接返回给前端,存储就需要前端想办法了。
前提是,你要把卡带在身上。
前端的存储方式有很多。
最矬的,挂到全局变量上,但这是个「体验卡」,一次刷新页面就没了
高端点的,存到 cookie、localStorage 等里,这属于「会员卡」,无论怎么刷新,只要浏览器没清掉或者过期,就一直拿着这个状态。
前端存储这里不展开了。有地方存了,请求的时候就可以拼到参数里带给接口了。
基石:cookie
可是前端好麻烦啊,又要自己存,又要想办法带出去,有没有不用操心的?
有,cookie。cookie 也是前端存储的一种,但相比于 localStorage 等其他方式,借助 HTTP 头、浏览器能力,cookie 可以做到前端无感知。一般过程是这样的:
在提供标记的接口,通过 HTTP 返回头的 Set-Cookie 字段,直接「种」到浏览器上
浏览器发起请求时,会自动把 cookie 通过 HTTP 请求头的 Cookie 字段,带给接口
「配置:Domain / Path」
你不能拿清华的校园卡进北大。
cookie 是要限制::「空间范围」::的,通过 Domain(域)/ Path(路径)两级。
Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如 www.example.com 会设为 example.com,而且以后如果访问example.com的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是域名必须一致。—— Cookie — JavaScript 标准参考教程(alpha)
「配置:Expires / Max-Age」
你毕业了卡就不好使了。
cookie 还可以限制::「时间范围」::,通过 Expires、Max-Age 中的一种。
Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式。如果不设置该属性,或者设为null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。如果同时指定了Expires和Max-Age,那么Max-Age的值将优先生效。如果Set-Cookie字段没有指定Expires或Max-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。—— Cookie — JavaScript 标准参考教程(alpha)
「配置:Secure / HttpOnly」
有的学校规定,不带卡套不让刷(什么奇葩学校,假设);有的学校不让自己给卡贴贴纸。
cookie 可以限制::「使用方式」::。
Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是Document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。—— Cookie — JavaScript 标准参考教程(alpha)
「HTTP 头对 cookie 的读写」 回过头来,HTTP 是如何写入和传递 cookie 及其配置的呢?HTTP 返回的一个 Set-Cookie 头用于向浏览器写入「一条(且只能是一条)」cookie,格式为 cookie 键值 + 配置键值。例如:
Set-Cookie: username=jimu; domain=jimu.com; path=/blog; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
那我想一次多 set 几个 cookie 怎么办?多给几个 Set-Cookie 头(一次 HTTP 请求中允许重复)
Set-Cookie: username=jimu; domain=jimu.com Set-Cookie: height=180; domain=me.jimu.com Set-Cookie: weight=80; domain=me.jimu.com
HTTP 请求的 Cookie 头用于浏览器把符合当前「空间、时间、使用方式」配置的所有 cookie 一并发给服务端。因为由浏览器做了筛选判断,就不需要归还配置内容了,只要发送键值就可以。
Cookie: username=jimu; height=180; weight=80
「前端对 cookie 的读写」 前端可以自己创建 cookie,如果服务端创建的 cookie 没加HttpOnly,那恭喜你也可以修改他给的 cookie。调用document.cookie可以创建、修改 cookie,和 HTTP 一样,一次document.cookie能且只能操作一个 cookie。
document.cookie = 'username=jimu; domain=jimu.com; path=/blog; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly';
调用document.cookie
也可以读到 cookie,也和 HTTP 一样,能读到所有的非HttpOnly
cookie。
console.log(document.cookie); // username=jimu; height=180; weight=80
(就一个 cookie 属性,为什么读写行为不一样?get / set 了解下)「cookie 是维持 HTTP 请求状态的基石」了解了 cookie 后,我们知道 cookie 是最便捷的维持 HTTP 请求状态的方式,大多数前端鉴权问题都是靠 cookie 解决的。当然也可以选用别的存储方式(后面也会多多少少提到)。那有了存储工具,接下来怎么做呢?
应用方案:服务端 session
现在回想下,你刷卡的时候发生了什么?
其实你的卡上只存了一个 id(可能是你的学号),刷的时候物业系统去查你的信息、账户,再决定「这个门你能不能进」「这个鸡腿去哪个账户扣钱」。
这种操作,在前后端鉴权系统中,叫 session。典型的 session 登陆/验证流程:
浏览器登录发送账号密码,服务端查用户库,校验用户
服务端把用户登录状态存为 Session,生成一个 sessionId
通过登录接口返回,把 sessionId set 到 cookie 上
此后浏览器再请求业务接口,sessionId 随 cookie 带上
服务端查 sessionId 校验 session
成功后正常做业务处理,返回结果
「Session 的存储方式」 显然,服务端只是给 cookie 一个 sessionId,而 session 的具体内容(可能包含用户信息、session 状态等),要自己存一下。存储的方式有几种:
Redis(推荐):内存型数据库,redis中文官方网站。以 key-value 的形式存,正合 sessionId-sessionData 的场景;且访问快。
内存:直接放到变量里。一旦服务重启就没了
数据库:普通数据库。性能不高。
「Session 的过期和销毁」很简单,只要把存储的 session 数据销毁就可以。 「Session 的分布式问题」 通常服务端是集群,而用户请求过来会走一次负载均衡,不一定打到哪台机器上。那一旦用户后续接口请求到的机器和他登录请求的机器不一致,或者登录请求的机器宕机了,session 不就失效了吗?这个问题现在有几种解决方式。
一是从「存储」角度,把 session 集中存储。如果我们用独立的 Redis 或普通数据库,就可以把 session 都存到一个库里。
二是从「分布」角度,让相同 IP 的请求在负载均衡时都打到同一台机器上。以 nginx 为例,可以配置 ip_hash 来实现。
但通常还是采用第一种方式,因为第二种相当于阉割了负载均衡,且仍没有解决「用户请求的机器宕机」的问题。「node.js 下的 session 处理」 前面的图很清楚了,服务端要实现对 cookie 和 session 的存取,实现起来要做的事还是很多的。在npm中,已经有封装好的中间件,比如 express-session - npm,用法就不贴了。这是它种的 cookie:
express-session - npm 主要实现了:
封装了对cookie的读写操作,并提供配置项配置字段、加密方式、过期时间等。
封装了对session的存取操作,并提供配置项配置session存储方式(内存/redis)、存储规则等。
给req提供了session属性,控制属性的set/get并响应到cookie和session存取上,并给req.session提供了一些方法。