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

背景

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

单应用的登录流程我们都了解,第一次登录前端将用户输入的账号密码发送给后台,后台如果验证通过,就返回一个凭证 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;
};
相关推荐
喵叔哟22 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django