写在前面的话
网上太多讲SSO的文章了,但是我觉得都不是那么的实用,看完脑袋可能还是一堆浆糊,望而生畏,我想用简单直白的方式给大家讲一讲。
这里只是以node层eggjs框架为演示基础,开发逻辑都是一样的,其他后端语言也一样的。
最重要的是先搞清楚做什么
有一些开发细节是要提前搞清楚的:首先是要搞清楚需求是做单点登录还是做免登录还是qiankun集成。(确实会有人搞不清楚的,QAQ)
一、这些都是必要的,没有就跟登录中心的人要。
登录中心URL地址、单点登录接口传输的标识(access_token)、access_token的校验接口以及相关参数(通常每个子应用会有对应的应用id以及secretCode)。
上面提到的这三点,在99%的单点登录场景都是会用到的,你问我剩下的1%?我也不知道。
登录中心URL地址:当用户直接访问子系统时,子系统发现当前用户是没有登录的,那就得跳转到登录中心地址去登录。
单点登录接口传输的标识: 这个就是登录中心给子系统下发的一个身份凭证。
access_token的校验接口以及相关参数:子系统收到身份凭证后需要去调用登录中心的接口查询用户的相关信息。
登出接口:从当前子系统登出登录中心使用,用于子系统通知登录中心登出。
很多ToB项目以子系统的身份接入到客户的现场的时候,客户还真就不让做登出的功能。 甚至客户会让把界面的登出按钮干掉,如果明确说不做登出了,那就不需要这个接口。
二、账号体系
因为登录中心的账号往往是有权限划分的。作为子系统接入登录中心时,要和登录中心确认好对于首次登录子系统的新用户的功能权限划分。通常分为三种:
- 新用户统一最大权限。(有的子系统比较低级)
- 三权分立。
- 新用户没有任何权限,需要首次登录后管理员添加权限。
三、交互细节,这三个问题得和登录中心确认好
如何获取到登录中心给的单点登录标识? 写一个单独的接口供登录中心调用?还是说传输token的参数名是子系统中之前之后都不会用到的,这一点要想好。
用户成功在子系统登录后调用业务的接口时,是否需要每一次请求都去登录中心查询一次登录状态,不要笑,真的有一些客户定制化需求会要求每次都去查,他的小脑袋瓜并不会去考虑登录中心的处理请求压力。
如果后续的每次请求不再需要去登录中心查询一次当前的登录状态的话,肯定就会出现一个疑问:当登录中心的账号退出时候,子系统如何感知到此时账号已经退出了呢?所以要确认好,是否需要提供子系统的登出接口,用于登录中心通知子系统登出。
如果都不需要做的话,那么会有一种有意思的bug发生:
举例: 用户A从登录中心成功通过单点登录进入子系统。登录中心用户A注销登录,登录用户B,但是子系统就不知道A注销了换成了B。此时再直接访问子系统,子系统从session中查到当前的状态仍然是之前的用户A在登录,此时就是登录中心明明登录的是B,但是子系统的操作记录都是A。
开干
网上单点登录流程的太多了,我就不赘述了,我就认为大家很清楚了。
一、拦截请求
一个简单的请求在平时是怎么处理的,请求来了,检查session,校验权限,校验参数,处理逻辑,返回结果。
那自然我们如果要加上单点登录,我们肯定得在请求一开始来的时候我们就对其进行拦截,所以我会选择加在middlware中的。作为一个中间件一开始就进行拦截。
module.exports = () => {
return async function wangzais98ksso(ctx, next) {
if (ctx.request.method === 'GET') {
try {
/ **
* @param ctx 请求上下文
* @return 三种情况 1. 登录中心地址 2. '/'重定向到首页 3.空,登录状态下的常规请求
*/
const redirectUrl = await getRedirectUrl(ctx);
if (redirectUrl) {
ctx.redirect(redirectUrl);
return;
}
} catch (e) {
// 记录错误日志
ctx.logger.error(e)
}
}
await next();
};
};
二、具体的重定向逻辑实现
思路步骤:
- 获取到代表身份的标识,我这里代码中用的access_token。
- 根据access_token获取用户信息,这里要传的参数根据接口调整。一些代表子系统身份的Id以及secretCode都是要跟随去验证的。
- 获取到用户信息后,查询是否为新用户。
- 本地存储新用户,这里就要注意权限问题。
- 设置session,使该用户处于登录状态,重定向到子系统首页。
如果一开始就没获取到access_token,发现是正常的接口访问的,那么就校验一下此时用户登录态是否过期即可,(正常子系统里面也会去校验一次的)
async function getRedirectUrl(ctx) {
// 获取到access_token,这里需要根据实际需求补全。是直接一个特殊的参数名,还是接口,我这里以一个特殊的参数名为例了。
const access_token = ctx.queries...。
if (access_token) {
// 根据access_token获取当前登录用的信息
const { data: userData = {} } = await ctx.curl(
`${sso_url}${接口}`,
{
method: 'GET',
timeout: 20000,
dataType: 'json',
rejectUnauthorized: false,
secureOptions: 2147485780,
data: {
access_token,
...一些其他的参数。
},
},
);
// 根据用户信息去查询,子系统的表里面之前有没有这个用户
const user = await ctx.model.users.find({
where: { userData.user_uuid) },
});
// 没有该用户就创建一个用户保存起来
if (!user) {
user = {
用户相关的一些字段信息
}
// 插入到表中
await ctx.model.users.upsert(user);
}
// 设置登录状态session
await setLocalSess(ctx, user);
// 重定向到首页
return '/';
}
// 没有access_token时则读取, 那说明此时可能已经处于登录态
const sess = await ctx.service.session.get();
if (sess && sess.userId) {
const now = +new Date();
const expire = sess.expire * 1000;
// 处于登录态,pass
if (expire > now) return;
}
// 未登录或者过期重定向登录中心
return loginUrl;
}
结语
这里给大家展示的是一个骨架,里面的血肉需要在实际开发中去补充,但是大同小异,思路都是一样的。