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 模式提高了认证中心对用户的控制能力,因为每隔一段时间 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", //签名使用的加密算法,HS256 typ: "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;function refreshToken() { if (promise) { return promise; } promise = new Promise(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的请求function isRefreshRequest(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)) { //刷新token const isSuccess = await refreshToken(); 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;});export default ins;