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", //签名使用的加密算法,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和refrestoken
import { 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;
转载自CSDN-专业IT技术社区
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_29425101/article/details/139860008