
单点登录(SSO)原理
单点登录(SSO)原理
前言
一个规模较大的公司大概率会有很多子系统,每个子系统都属于公司,没必要为每个子系统做一个登录系统,因为用户是相通的,把每个子系统的登录部分抽离出来形成一个认证中心,这就是单点登录(Single Sign-On,SSO)。
实现单点登录的模式比较多,并没有固定的模式,不过有标准模式(CAS、OAuth2),非标准模式,可能每个公司实现方案都不相同。但从技术上来看,大致可以分为两种:一个是 session + cookie
,一个是 token
。
单点登录的常见模式
Session + Cookie
用户将账号密码信息发给认证中心,认证中心有个 session
表格,里面是键值对,key
是生成的全局唯一 id
,value
就是用户的身份信息。一旦用户登录成功,表格里面就会记录一条信息。
只要认证中心的
session
表有这个用户的信息,那么就表明该用户是登录成功的状态,反之,session
表没有这个信息,用户就会登录失效,有可能是过期了,session
表有可能是存在数据库的,也有可能是存在 Redis(内存)中。
利用 cookie
把 sid
带给用户,浏览器就会保存这个 sid
,后面浏览器访问子系统时就会把 sid
带过去。子系统并没有这个 session
表去判定是否有效,于是子系统会将接收到的 sid
发给认证中心,认证中心去查,查到后会告诉子系统该用户完成登录了,具有权限,把身份信息给到子系统。
这种模式下有个好处就是认证中心的控制力很强,只要 session
表删除了用户信息,用户就会立马下线,再配合黑名单,用户就登不上系统了。
这种模式下只要用户体量很大,认证中心的压力就会非常大,不同子系统不断地给认证中心发 sid
让他判断,并且表也会非常庞大,还要做 session
集群,并且认证中心不能挂,你需要给他做一个 session
容灾;再者,某个子系统的用户体量很大导致该系统要扩容,这样一来,这个子系统给认证中心发 sid
的频率也在变大,随之认证中心也要扩容。这里面所有的缺陷最终都是指向烧钱,为了降低成本,token
模式随之诞生。
Token
这个模式下的认证中心压力很小。
token
模式下用户向认证中心发送登录信息后,认证中心此时并没有向 session
表去记录任何东西,认证中心会生成一个不能篡改的字符串 token
给用户,这其实就是 jwt
,此前文章有详细讲过 juejin.cn/post/734643R。
用户接收到 token
后会将 token
存入,可以存 cookie
也可以存入 localStorage
都可以,后面的事情就无关认证中心了。于是用户访问某个子系统时带上 token
,子系统是可以自己认证的,具体认证方式比如子系统和认证中心去交换一个密钥,子系统拿到一个密钥之后可以自行认证用户的 token
是否为认证中心颁发的,一旦认证成功就会把受保护的资源发给用户。
由此可见,token
模式下认证中心压力就很小时,因为子系统几乎没有向认证中心发送任何的请求,成本随之降低,具体某个子系统用户体量大而去扩容也不会影响到认证中心。
缺点也很明显,认证中心失去了对用户的绝对控制,假设某个用户违规操作,现在希望让这个用户下线,就需要认证中心向每个子系统去发送信息,让用户下线,一两个子系统倒还好,一旦子系统过多就很麻烦。
为了解决这个问题,双 token
随之诞生。
Token + RefreshToken
这个模式下有两个 token
,一个原 token
一个刷新 token
。
用户在登录完成后,认证中心会发送两个 token
,一个 token
是所有子系统都能识别的,另一个刷新 token
只有认证中心自己认识,原 token
的刷新时间非常短,可能 20min 刷新一次,刷新 token
的过期时间会比较长,比如一周一个月。
假设原 token
没有失效,那么流程就和单 token
模式一样。假设失效了就会如下:
用户第一次登录会收到认证中心的两个 token
,假设用户过了一段时间去登录子系统,原 token
过期了,子系统告诉这个 token
失效,此时用户会将刷新 token
发给认证中心去验证,认证中心会返回一个新的 token
给到用户,用户再去访问子系统就可以正常访问了。
这个模式的意义相较于单 token
模式多了层对用户的控制,比如某个用户违规操作希望让其下线,虽然不能让该用户立即下线,但是原 token
一旦过期,用户拿着刷新 token
向认证中心索要 token
时,认证中心不管就行,其余子系统是无感的。
Token 的无感刷新
token
的无感刷新其实主要工作在于后端。
看个情形:一般 token
过期时间很短,假设 token
过期时间为 10min,用户登录 10min 后 token
失效就会把你送回登录界面重新登录,此时查看 request
你会发现其实是携带了 token
的,只不过你的 token
失效了,因此 401 了。每次 10min 后用户都要重新登录下,这样用户体验很糟糕。
如何解决这个问题呢,我们可以加一个刷新 token
,也就是 refreshToken
,这个 token
的过期时间一般会设置较长比如一周、两周、一个月,这个 token
的作用就是去给你替换新的原 token
。
所谓 token
无感刷新就是让你原 token
过期时,前端默默帮你把 refreshToken
替换成了新 token
,用户不再需要重新登陆去拿到新的原 token
。
所以前端想要实现无感刷新的基本思路就是当原 token
过期时,用 refreshToken
替换原 token
,写一个 refreshToken
函数即可,需要去封装 axios
,拦截器 interceptors
。
这里我也简单在此前 token
文章中的例子基础上实现下无感刷新:
import axios from 'axios' |
这是那期文章 axios
的封装,这是没有 refreshToken
的情况,现在去增加一个 refreshToken
。也就是 401 时,去刷新 token
再去重新请求:
// 刷新 token |
这样写其实有个问题会陷入死循环,res.config
里面的 token
还是沿用的失效的 token
,因此还需要改下:
// 刷新 token |
其实当前写法还是有个问题,当刷新 token
也过期后,依旧会陷入死循环,我们可以在 refreshToken
中加一个条件参数 isRefreshToken
,然后判断 401 时加上这个条件即可。另外需要刷新成功才走重新请求,最终写法如下:
import axios from 'axios' |
当网速比较慢时,refreshToken
耗时,此时又有其余的请求加入,于是 refreshToken
就会产生多个 promise
造成冗余,此时就可以对 refreshToken
进行优化,如下:
let promise |
OAuth2 协议
OAuth 1.0 版本几乎已经不用了,这里不会概述。
OAuth2 协议其实就是你登录第三方网站,这个网址支持你可以通过微信、Apple、Google、GitHub 等工具去登录,这样,对于你不信任的网站登录时你不需要提供账号密码,这样的方式就可以避免泄露自己的账号密码等信息。
OAuth2 的认证流程如下:
假设用户现在通过微信去登录 LeetCode,用户只要同意授权,那么认证服务器就会给第三方网站 LeetCode 颁发 token
,同意之后,LeetCode 就可以拿到你的头像等等信息。
其实所谓的身份认证,其本质都是基于对请求方的不信任产生的,因此 OAuth2 就是来解决这个问题的。
还有个问题就是像是微信这样扮演认证服务器的角色不可能给所有的第三方站点都提供这个 token
,因此第三方站点需要向微信申请第三方应用,一般微信、微博、Apple、GitHub 都会有自己的 OAuth 使用说明。
OAuth2 有几种授权方式,这部分内容以后有空再填。
最后
一般来讲,小规模系统 Session + Cookie
够用,大规模系统就适合 Token
或者 双Token
,若是需要第三方登录就用 OAuth2
。