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

背景

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

单应用的登录流程我们都了解,第一次登录前端将用户输入的账号密码发送给后台,后台如果验证通过,就返回一个凭证 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;
};
相关推荐
IT_陈寒几秒前
用了Vue的动态组件之后,我被坑得找不着北
前端·人工智能·后端
likerhood5 分钟前
ConcurrentHashMap底层数据结构和面试常见问题
java·数据结构·面试·hashmap
阳火锅30 分钟前
💡 告别类名地狱!Tailwind CSS 语义化转换神器来了
前端·css·vue.js
ricardo197333 分钟前
Core Web Vitals 全解:LCP / INP / CLS 逐个击破
前端
VillenK35 分钟前
版本依赖问题:vite-plugin-dts@3.1.0 与 jiti 的兼容性
前端·typescript·vite
Languorous.43 分钟前
C++数据结构高阶|布隆过滤器(Bloom Filter)深度解析:从原理到手写实现,面试高频考点全覆盖
数据结构·c++·面试
Apifox1 小时前
如何在 Apifox 中快速构建和调试 AI Agent
前端·agent·ai编程
一晌贪欢i1 小时前
WebContainer 重点介绍
前端·webcontainer
山河木马1 小时前
Emscripten 从 C/C++ 调用 JavaScript
前端·javascript·c++
鹏程十八少1 小时前
12. Android 协程通关秘籍:31 道资深工程师面试题精讲
前端·后端·面试