1. 什么是单点登录?
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
2. 单点登录的模式
2.1 模式一: session + cookie 模式

- 用户登录
- 认证中心获取用户数据,在 session 表中建立 sessionId 与用户信息的键值对
- 将 sessionId 以 cookie 的形式返回返回用户,并保存在客户端
- 当用户访问系统 A 时,将 cookie 中的 sessionId 附带在请求头中进行数据请求
- 系统 A 获取到用于户的 sessionId,需要去认证中心验证 sessionId,看用户是否登录或者登录是否过期
- 认证中心验证 sessionId 是有效的,返回验证结果,也可以附带用户身份信息到系统 A
- 系统 A 接收到认证中心的有效验证,将用户访问的受保护的资源响应给客户端
- 对其他系统的访问也是一样
优势:
认证中心对用户有着非常强的控制力:如果某个用户违规了不让其继续使用系统了,可以在认证中心维护的 session 记录中清除该用户的 sessionId,当用户请求系统时 sessionId 验证不通过,就会要求重新登录,如果配合黑名单使用控制会更灵活。
劣势:
认证中心压力大
系统登录和所有的请求验证都需要认证中心验证
花费大,代价高
- 储存用户 session 信息,如果系统用户非常大,需要做 session 集群。
- 认证中心管控登录及所有的请求验证,如果挂了,整个系统都不能用了,需要做容灾。
- 如果某个系统需要扩容,认证中心也跟着需要扩容。
2.2 模式二:token 模式

- 用户登录
- 登录成功,认证中心生成一个不能被篡改的字符串 token,返回给用户并保存在客户端。
- 用户访问系统 A,会在请求头(或者 url 中)中附带 token 信息,系统 A 可以自行进行验证 token
- 系统 A 验证通过,返回系统受保护的资源给客户端
- 对其他系统的访问也是一样
优势:
- 认证中心的压力减少了,只需要进行注册登录,不需要进行请求验证了,子系统可以自行验证,减少了认证中心的压力。
- 花费减少了,认证中心不需要维护 sessionId 记录了。请求不需要对认证中心的验证,对认证心中的要求也不那么高了。子系统扩容也不会影响认证中心了。
劣势:
认证中心失去了对用户的控制,用户登录之后不需要经过认证中心就可以访问系统资源了。
2.3 模式二的升级:token + refreshtoken(双 Token)模式


- 用户登录
- 登录成功,认证中心会生成一个 token(有效期比较短)和一个 refreshToken(有效期比较长),返回给用户并保存在客户端。
- 用户访问系统 A,会在请求头中附带 token 信息,系统 A 可以自行进行验证 token
- 系统 A 验证通过,返回系统受保护的资源给客户端
- 当 token 过期了再请求系统 A 时,系统 A 验证不通过。用户可以使用 refreshToken 到认证中心换取新的 token,并更新保存在客户端。后续的请求使用新的 token 又可以正常访问系统了。
- 当 refreshtoken 过期了,可以重新登录或者做其他逻辑处理。
双 Token 模式提高了认证中心对用户的控制能力,因为每隔一段时间 token 失效后,用户需要到认证中心获取新 token。该模式弥补了 token 模式的缺点,又保持了 token 模式的优势。
3. 客户端对 cookie 或者 token 的保存
登录认证后认证中心返回的 cookie(或者 token),需要在客户端保存。
- 如果系统 A、系统 B 和认证中心是在相同的主域名下,可以通过设置 set-cookie 中的 domain 为主域名,path 设置为根路径,实现后续对系统 A、B 的请求中请求头会自动携带 cookie 。因为设置域名将会使 cookie 对指定的域名及其所有子域名可用。
- 如果系统 A、系统 B 不相同的主域名下,因为浏览器的同源策略,在不同源的请求中浏览器会限制 cookie 的发送,此时可以将 cookie 信息保存在 localStorage 中,以解决跨域请求 cookie 发送。
set-cookie:是响应标头,内容信息由服务器设置,响应返回,客户端保存。在客户端后续的请求中,根据登录响应头 set-cookie 的内容限制,决定后续请求头中是否要附带 cookie。set-cookie 中包含 cookie 内容,也包含 cookie 的其他信息,比如 Max-age(有效期)、domain(请求时 cookie 可以发送的域)、SameSite(跨站时携带 cookie 的约束)等。
4. JWT (JSON Web Token)
JWT 是一种特殊的 Token,由三个部分组成(header、payload、signature),中间用符号.连接 。
header 部分:由一个 JSON 对象,进行 base64 编码得到。
let header ={alg:"HS256",//签名使用的加密算法,HS256typ:"JWT",//一般是固定的};
header =btoa(JSON.stringify(header));//进行base64编码
payload 部分:主体内容部分,也是一个 JSON 对象,也要进行 base64 编码。
let payload ={name:"admin",age:18,};
payload =btoa(JSON.stringify(payload));
signature 部分:由前面 base64 编码得到的两个部分用.拼接,进行加密(需要一个秘钥),再进行 base64 编码得到。
token 是由服务器生成,返回给客户端并保存,在后续的请求中携带 token 进行请求。系统进行 token 验证时,解析出 token 中的 header 和 payload,用相同的秘钥进行加密处理后,再和 token 中的 signature 部分对比,如果相同则通过验证,不同则表示 token 被篡改了或者客户端没有进行登录伪造的。在单点登录模式二中,认证中心生成秘钥进行加密,可以将秘钥同步给系统 A、系统 B,这样系统 A、B 就可以自行验证 token 的有效性了。
5. 双 token 无感刷新
双 token 模式下 token 有效时间较短,refreshToken 的有效时间较长,当 token 失效后用 refreshToken 请求获取新的 token。当 refreshToken 也过期失效后,需要重新登录。
import axios from"axios";//set将token和refreshtoken保存到localstorage//get为从localstorage中取出token和refrestokenimport{ getToken, setToken, getFreshToken, setFreshToken }from"./token.js";const ins = axios.create({baseUrl:"http://localhost:3000",headers:{Authorization:`Bearer ${getToken()}`,},});//刷新token的请求let promise;functionrefreshToken(){if(promise){return promise;}
promise =newPromise(async(resolve)=>{const resp =await ins.get("/refresh_token",{headers:{Authorization:`Bearer ${getFreshToken()}`,},__isRefreshToken:true,});if(resp.data.code ===200){//刷新token成功resolve(true);}else{//刷新token失败,refreshToken也过期失效了resolve(false);}});
promise.finally(()=>{
promise =null;});return promise;}//判断是否是刷新token的请求functionisRefreshRequest(config){return!!config.__isRefreshToken;}
ins.interceptors.response.use(async(res)=>{if(res.headers.Authorization){const token = res.headers.Authorization.replace("Bearer ","");setToken(token);
ins.defaults.headers.Authorization =`Bearer ${token}`;}if(res.headers.refreshtoken){const freshtoken = res.headers.refreshtoken.replace("Bearer ","");setFreshToken(freshtoken);}//无权限,且不是刷新token请求,是普通数据请求if(res.data.code ===401&&!isRefreshRequest(res.config)){//刷新tokenconst isSuccess =awaitrefreshToken();if(isSuccess){//刷新成功,重新请求
res.config.headers.Authorization =`Bearer ${getToken()}`;const resp =await ins.request(res.config);return resp;}else{//刷新token失败,refreshToken过期了
console.log("需要重新登录");}}return res.data;});exportdefault ins;
版权归原作者 小禄不迷路 所有, 如有侵权,请联系我们删除。