单点登录的功能设计及实践

背景

🤔 企业多应用的登录问题?单点登录有什么好处呢?

单应用的登录流程我们都了解,第一次登录前端将用户输入的账号密码发送给后台,后台如果验证通过,就返回一个凭证 token,之后前端在每次请求中携带这个 token 就可以正常访问了。

但是对于企业中多应用的场景,如果每个应用仍然使用上面的登录流程,则会显得繁琐。一是各个应用后台都需要维护用户的账号密码信息,二是用户对各个应用都需要输入账号密码登录 。而使用单点登录( Single Sign On ,简称 SSO),用户只需要登录一次就可以访问所有相互信任的应用系统

单点登录示意

下面演示了单点登录的流程。视频中有应用A,应用B,以及统一登录服务 CAS(Central Authentication Service)。

用流程图来描述就是下面这样。需要注意的是,第一次应用A跳转CAS,需要用户输入用户名密码,而第二次应用B跳转CAS,就直接登录成功了。

具体流程

第一次访问应用1

  1. 用户访问应用1,应用1的服务器验证用户是否登录,未登录则重定向到 CAS 登录页面
  2. 用户没有 CAS 的权限,需要填写登录表单,将用户名和密码发送到 CAS 的后端
  3. CAS 的后端对用户信息进行验证,验证通过则返回 CAS 的 token(表示用户已登录 CAS 系统),同时返回一个ticket
  4. CAS 登录成功后,又重定向到应用1的页面,同时在 URL 中带上上一步的ticket 。应用1页面再次访问应用1的服务器,服务器获取这个ticket,并向 CAS 进行验证。
  5. 若 CAS 验证 ticket 有效,则向应用服务器发送这个用户的必要属性(如 userId),并表示验证成功。这时应用服务器可以明确当前访问用户可信,并生成临时登录凭证 token 返回给前端。
  6. 之后应用1的前端可直接通过这个 token 访问应用1服务器。

第一次访问应用2

第一次访问应用2与前面的流程差别不大,唯一的不同是在跳转 CAS 时,CAS 的前端已经存有 CAS 的登录凭证token ,这时就不需要用户再次输入用户名密码了。其中需要关注两点:

  1. 所有的登录过程都依赖于 CAS 服务,包含用户登录页面、ticket 生成及验证。
  2. 为了保证 ST(上文的 ticket) 的安全性,一般 ST 都是随机生成的,没有规律性。CAS 规定 ST 只能保留一定的时间,之后 CAS 服务会让它失效,而且,CAS 协议规定 ST 只能使用一次,无论 ST 验证是否成功,CAS 服务都会清除服务端缓存中的该 ST,从而规避同一个 ST 被使用两次或被窃取的风险。

代码实现

我们需要关注两个点,一是 CAS 相对于普通登录需要做哪些工作,二是接入应用相对于普通登录需要做哪些改变。

我们还是根据上文的流程来看一步步如何实现(后端使用 Node.js)。

应用跳转 CAS

第一次访问应用(未登录),需要跳转 CAS 进行登录。在示例中是后端进行重定向,但是由于我们是单页应用,静态文件和接口服务实际上是不同的地址,所以不能在访问 example.com 时后端直接重定向到 cas.com。 替代方案是,后端判断未登录返回跳转地址,在前端进行重定向。

CAS 登录

其他应用在跳转 CAS 时会携带一个回调url,用于在登录成功后返回原应用。在 CAS 的登录接口中,验证用户信息成功后不仅需要返回 token,还需要返回回调的 url,并带上一个 passport 参数 (用于之后的验证)。

JS 复制代码
const redirectUrl = req.query.redirect_uri;

if (redirectUrl) {
  response.result.redirectUrl = appendQueryToUrl(redirectUrl, {
    passport: createPassport(user.userId), // 在回调地址中添加 passport 参数
  });
}

CAS 前端如果接收到返回中存在 redirectUrl,则直接跳转。

JS 复制代码
if (data.result.redirectUrl) {
  window.location.replace(data.result.redirectUrl);
}

passport 验证

CAS 签发的 passport 是一个临时凭证,应用后端可以通过这个 passport 向 CAS 进行验证,如果验证成功,CAS 会返回这个用户的必要信息。

JS 复制代码
 const { passport } = req.query;
  if (passport) {
    try {
      const data = await verifyPassport(passport); // 向 CAS 验证 passport 是否有效
      if (data.status.code === 0) {
        // passport 有效,用户认证成功
        const { result } = data;
        req.currentUser = {
          userId: result.userId,
          username: result.username,
        };
        // 设置浏览器端 cookie
        const accessToken = sign(req.currentUser);
        res.cookie('app_a_access_token', accessToken, {
          expires: new Date(Date.now() + 8 * 3600000),
        });
        return next();
      }
    } catch (err) {
      console.error(err);
    }
  }

CAS 中,passport 被设计为只能使用一次,并且具有时效。

JS 复制代码
const passports = {};

// 创建 passport 
const createPassport = (userId) => {
  const expireTime = new Date().getTime() + 1000 * 60; // 设置一分钟后过期
  const pId = uuid();
  passports[pId] = {
    userId,
    expireTime,
  };
  return pId;
};

// 获取 passport 内容
const getPassportContent = (pid) => {
  if (!pid || !passports[pid]) return;
  if (passports[pid].expireTime < new Date().getTime()) {
    // 凭证过期
    delete passports[pid]; // 删除凭证
    return;
  }  

  const res = {
    userId: passports[pid].userId,
  };
  delete passports[pid]; // 删除凭证
  return res;
};
相关推荐
FakeOccupational几秒前
【树莓派 002】 RP2040 实现示波器 PIO来驱动 ADC10080 并抓取数据方案+ 内置12-bitADC&DMA&网页前端可视化方案
前端
至善迎风4 分钟前
Bun:下一代 JavaScript 运行时与工具链
开发语言·javascript·ecmascript·bun
DJ斯特拉6 分钟前
Vue工程化
前端·javascript·vue.js
秋深枫叶红7 分钟前
嵌入式第三十五篇——linux系统编程——exec族函数
linux·前端·学习
LinDon_13 分钟前
【vue2form表单中的动态表单校验】
前端·javascript·vue.js
一水鉴天23 分钟前
整体设计 之28 整体设计 架构表表述总表的 完整程序(之27 的Q268 )(codebuddy)
java·前端·javascript
DsirNg29 分钟前
使用 SSE 单向推送实现 系统通知功能
前端·javascript
IT_陈寒38 分钟前
SpringBoot 3.2 实战:用这5个新特性让你的API性能提升40%
前端·人工智能·后端
霍理迪1 小时前
HTML初相识
前端·html
恋猫de小郭1 小时前
Android 宣布 Runtime 编译速度史诗级提升:在编译时间上优化了 18%
android·前端·flutter