前端鉴权:cookie、session、token、jwt、单点登录全贡献出来了!
本文你将看到:
基于 HTTP 的前端鉴权背景
cookie 为什么是最方便的存储方案,有哪些操作 cookie 的方式
session 方案是如何实现的,存在哪些问题
token 方案是如何实现的,如何进行编码和防篡改?jwt 是做什么的?refresh token 的实现和意义
session 和 token 有什么异同和优缺点
单点登录是什么?实现思路和在浏览器下的处理
从状态说起
HTTP 无状态
我们知道,HTTP 是无状态的。也就是说,HTTP 请求方和响应方间无法维护状态,都是一次性的,它不知道前后的请求都发生了什么。
但有的场景下,我们需要维护状态。最典型的,一个用户登陆微博,发布、关注、评论,都应是在登录后的用户状态下的。
那解决办法是什么呢?标记
。
在学校或公司,入学入职那一天起,会录入你的身份、账户信息,然后给你发个卡,今后在园区内,你的门禁、打卡、消费都只需要刷这张卡。
前端存储
这就涉及到一发、一存、一带,发好办,登陆接口直接返回给前端,存储就需要前端想办法了。
前提是,你要把卡带在身上。
前端的存储方式有很多。
最矬的,挂到全局变量上,但这是个「体验卡」,一次刷新页面就没了
高端点的,存到 cookie、localStorage 等里,这属于「会员卡」,无论怎么刷新,只要浏览器没清掉或者过期,就一直拿着这个状态。
有地方存了,请求的时候就可以拼到参数里带给接口了。
cookie 也是前端存储的一种,但相比于 localStorage 等其他方式,借助 HTTP 头、浏览器能力,cookie 可以做到前端无感知。
浏览器发起请求时,会自动把 cookie 通过 HTTP 请求头的 Cookie 字段,带给接口
「空间范围」
的,通过 Domain(域)/ Path(路径)两级。「时间范围」
,通过 Expires、Max-Age 中的一种。Max-Age 属性指定从现在开始 Cookie 存在的秒数,比如60 60 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。
如果同时指定了 Expires 和 Max-Age ,那么 Max-Age 的值将优先生效。
如果 Set-Cookie 字段没有指定 Expires 或 Max-Age 属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。
「使用方式」
。HttpOnly 属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是Document.cookie 属性、XMLHttpRequest 对象和 Request API 都拿不到该属性。这样就防止了该 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: username=jimu; domain=jimu.com
Set-Cookie: height=180; domain=me.jimu.com
Set-Cookie: weight=80; domain=me.jimu.com
Cookie: username=jimu; height=180; weight=80
cookie
,如果服务端创建的 cookie
没加HttpOnly
,那恭喜你也可以修改他给的 cookie
。调用
document.cookie
可以创建、修改 cookie
,和 HTTP
一样,一次document.cookie
能且只能操作一个 cookie
。document.cookie
也可以读到 cookie
,也和 HTTP
一样,能读到所有的非HttpOnly cookie
。console.log(document.cookie);
// username=jimu; height=180; weight=80
cookie
后,我们知道 cookie
是最便捷的维持 HTTP
请求状态的方式,大多数前端鉴权问题都是靠 cookie
解决的。当然也可以选用别的存储方式(后面也会多多少少提到)。应用方案:服务端 session
典型的 session 登陆/验证流程:
浏览器登录发送账号密码,服务端查用户库,校验用户
服务端把用户登录状态存为 Session,生成一个 sessionId
通过登录接口返回,把 sessionId set 到 cookie 上
此后浏览器再请求业务接口,sessionId 随 cookie 带上
服务端查 sessionId 校验 session
成功后正常做业务处理,返回结果
Redis(推荐):内存型数据库,redis中文官方网站。以 key-value 的形式存,正合 sessionId-sessionData 的场景;且访问快。
内存:直接放到变量里。一旦服务重启就没了
数据库:普通数据库。性能不高。
一是从「存储」角度,把 session 集中存储。如果我们用独立的 Redis 或普通数据库,就可以把 session 都存到一个库里。
二是从「分布」角度,让相同 IP 的请求在负载均衡时都打到同一台机器上。以 nginx 为例,可以配置 ip_hash 来实现。
npm
中,已经有封装好的中间件,比如 express-session - npm
,用法就不贴了。封装了对cookie的读写操作,并提供配置项配置字段、加密方式、过期时间等。 封装了对session的存取操作,并提供配置项配置session存储方式(内存/redis)、存储规则等。 给req提供了session属性,控制属性的set/get并响应到cookie和session存取上,并给req.session提供了一些方法。
应用方案:token
门卫小哥直接对照我和学生证上的脸,确认学生证有效期、年级等信息,就可以放行了。
这种方式通常被叫做 token。
用户登录,服务端校验账号密码,获得用户信息
把用户信息、token 配置编码成 token,通过 cookie set 到浏览器
此后用户请求业务接口,通过 cookie 携带 token
接口校验 token 有效性,进行正常业务接口处理
base64
比如 node 端的 cookie-session - npm 库
eyJ1c2VyaWQiOiJhIn0=
,就是 {"userid":"abb”}
的 base64 而已secret: 'iAmSecret',
signed: true,
.sig cookie
,里面的值就是 {"userid":"abb”}
和 iAmSecret通过加密算法计算出来的,常见的比如HMACSHA256
类 (System.Security.Cryptography) | Microsoft Docs。
eyJ1c2VyaWQiOiJhIn0=
,但伪造不出 sig 的内容,因为他不知道 secret。cookie
数量,数据本身也没有规范的格式,所以 JSON Web Token Introduction - jwt.io
横空出世了。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJhIiwiaWF0IjoxNTUxOTUxOTk4fQ.2jf3kl_uKWRkwjOP6uQRJFqMlwSABcgqqcJofFH5XCo
access token 用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活
refresh token 用来获取 access token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,也可以如前面的 session 一样处理
session 和 token
cookie 是浏览器在域下自动携带的,这就容易引发 CSRF 攻击。
存数据:请求只需携带 id,可以大幅缩短认证字符串长度,减小请求体积
不存数据:不需要服务端整套的解决方案和分布式处理,降低硬件成本;避免查库带来的验证延迟
单点登录
“真实”的单点登录(主域名不同)
这要能实现「一次登录,全线通用」,才是真正的单点登录。
这种场景下,我们需要独立的认证服务,通常被称为 SSO。
用户进入 A 系统,没有登录凭证(ticket),A 系统给他跳到 SSO
SSO 没登录过,也就没有 sso 系统下没有凭证(注意这个和前面 A ticket 是两回事),输入账号密码登录
SSO 账号密码验证成功,通过接口返回做两件事:一是种下 sso 系统下凭证(记录用户在 SSO 登录状态);二是下发一个 ticket
客户端拿到 ticket,保存起来,带着请求系统 A 接口
系统 A 校验 ticket,成功后正常处理业务请求
此时用户第一次进入系统 B,没有登录凭证(ticket),B 系统给他跳到 SSO
SSO 登录过,系统下有凭证,不用再次登录,只需要下发 ticket
客户端拿到 ticket,保存起来,带着请求系统 B 接
在 SSO 域下,SSO 不是通过接口把 ticket 直接返回,而是通过一个带 code 的 URL 重定向到系统 A 的接口上,这个接口通常在 A 向 SSO 注册时约定浏览器被重定向到 A 域下,带着 code 访问了 A 的 callback 接口,callback 接口通过 code 换取 ticket
这个 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只为了传一下换 ticket,换完就失效
callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
在后续请求中,只需要把 cookie 中的 ticket 解析出来,去 SSO 验证就好
访问 B 系统也是一样
总结
HTTP 是无状态的,为了维持前后请求,需要前端存储标记
cookie 是一种完善的标记方式,通过 HTTP 头或 js 操作,有对应的安全策略,是大多数状态管理方案的基石
session 是一种状态管理方案,前端通过 cookie 存储 id,后端存储数据,但后端要处理分布式问题
token 是另一种状态管理方案,相比于 session 不需要后端存储,数据全部存在前端,解放后端,释放灵活性
token 的编码技术,通常基于 base64,或增加加密算法防篡改,jwt 是一种成熟的编码方案
在复杂系统中,token 可通过 service token、refresh token 的分权,同时满足安全性和用户体验
session 和 token 的对比就是「用不用cookie」和「后端存不存」的对比
单点登录要求不同域下的系统「一次登录,全线通用」,通常由独立的 SSO 系统记录登录状态、下发 ticket,各业务系统配合存储和认证 ticket