单点登录SSO原理简析与接入实践

Daotin 于 2022-12-31 发布 编辑
  1. 背景
  2. 单点登录过程
    1. 方式一
    2. 方式二
  3. 单点注销过程
    1. 方式一
    2. 方式二
  4. 系统接入单点登录实践
  5. 参考链接

背景

在企业发展初期,企业使用的系统很少,通常一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录,很方便。但随着企业的发展,用到的系统随之增多,运营人员在操作不同的系统时,需要多次登录,而且每个系统的账号都不一样,这对于运营人员来说,很不方便。于是,就想到是不是可以在一个系统登录,其他系统就不用登录了呢?这就是单点登录要解决的问题。

单点登录英文全称Single Sign On,简称就是SSO。是目前比较流行的企业业务整合的解决方案之一。它的解释是:在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。

如图所示,图中有 4 个系统,分别是 Application1、Application2、Application3、和 SSO。Application1、Application2、Application3 没有登录模块,而 SSO 只有登录模块,没有其他的业务模块,当 Application1、Application2、Application3 需要登录时,将跳到 SSO 系统,SSO 系统完成登录,其他的应用系统也就随之登录了。

单点登录过程

方式一

描述:当单点登录到的子系统都是一家公司的时候使用。

特点:子系统没有自己的登录系统,所有的登录都需要走 SSO

比如 App1 和 App2 都是一家公司的,那么当登录了 App1 后再去进入 App2 的时候,会自动登录。

相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。

间接授权通过令牌(authCode)实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌(authCode),在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌authCode, 然后各个子系统通过单独的单点登录接口,传入 authCode,得到系统登录的 Token,完成登录过程。

这个过程,也就是单点登录的原理,用下图说明:

具体流程如下:

  1. 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  2. sso认证中心发现用户未登录,将用户引导至登录页面
  3. 用户输入用户名密码提交登录申请
  4. sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌authCode
  5. sso认证中心带着令牌跳转会最初的请求地址(系统1)
  6. 系统1拿到令牌authCode,去sso认证中心校验令牌是否有效
  7. sso认证中心校验令牌,返回有效,注册系统1
  8. 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  9. 用户访问系统2的受保护资源
  10. 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
  11. sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
  12. 系统2拿到令牌,去sso认证中心校验令牌是否有效
  13. sso认证中心校验令牌,返回有效,注册系统2
  14. 系统2使用该令牌创建与用户的局部会话,返回受保护资源

用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系:

  1. 局部会话存在,全局会话一定存在
  2. 全局会话存在,局部会话不一定存在
  3. 全局会话销毁,局部会话必须销毁

方式二

描述:当单点登录到的子系统是不同的公司的时候。

特点:子系统有自己的登录系统,即使没有 SSO 也可以单独登录,SSO 只相当于一个跳板。

如果在子系统登录,走的就是子系统的登录方式,如果想走集中登录,那么登录的入口就是集中登录平台,然后从集中登录平台进入 A 公司的 App1系统时,会携带 authCode,然后在App1系统单独的单点登录接口进行校验 authCode(而不必到 SSO 去校验),然后拿到 Token,完成 App1 的登录。

B 公司的 App2 也是一样的登录过程。

但是如果登录了 App1,再直接跳转到App2 是不能直接进入的,必须从集中登录平台才可以进入。

单点注销过程

方式一

单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁,用下面的图来说明:

下面对上图简要说明

  1. 用户向系统1发起注销请求
  2. 系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
  3. sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
  4. sso认证中心向所有注册系统发起注销请求
  5. 各注册系统接收sso认证中心的注销请求,销毁局部会话
  6. sso认证中心引导用户至登录页面

sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作。

方式二

由于方式二中,各个子系统都有自己的登录,注销过程。SSO 登录只是一个跳板而已,所以方式二的注销过程直接调用子系统的注销方式即可,然后window.close()关闭界面即可。

系统接入单点登录实践

方式二为例,介绍下一个系统如何集成单端登录功能。

具体流程如下:

  1. 首先会有一个集中登录平台,需要先登录集中登录平台
  2. 当集中登录平台登录后,点击子系统会跳转到子系统
  3. 跳转到子系统的时候会携带 authCode 参数在地址栏上
  4. 子系统需要在全局路由钩子 router.beforeEach 里面进行判断是否有authCode
  5. 如果有 authCode,则走集中登录的接口,否则走正常系统登录的逻辑
  6. 在退出的时候,直接调用系统的登出接口,然后使用window.close()关闭界面即可

代码也比较简单:

首先会有一个单点登录的接口,接口的传参可以是一个 authCode,或者有多个参数。

返回值就是我们系统登录完成需要的 Token。

全局路由守卫:

// 导航守卫
router.beforeEach(async (to) => {
  try {
    const title = (to.meta && (to.meta.title as string)) || ''
    if (title) {
      document.title = '商户平台-' + title
    }
    const appStore = useAppStoreWithOut();

    // 集中平台登录
    const authCode = to.query.authCode;
    if (authCode) {
      await appStore.ssoLogin(authCode)
      return false;
    }

    const token = localMng.getItem(TokenName)
    const outerPaths = getTreeNodeValue([portalRoute, ...payRoute], 'path')
    // token不存在(说明没登录),但是路由将要进入系统内部,自动跳到登录页面。
    if (!token && !outerPaths.includes(to.path)) {
      if (to.path == '/login' || to.path == '/') {
        return;
      }
      return '/login'
    } else {
      // 如果token存在(说明已登录),但是用户信息不存在,这时应该获取用户信息(用于处理刷新页面的情况)
      if (token && !appStore.userInfo.id) {
        await Promise.all([appStore.getUserInfo(), appStore.getMenuList()]);
        return { ...to, replace: true }
      }
    }
  } catch (error) {
    console.error(error)
  }
})

登录接口:

登录完成后,设置 Token到本地缓存,设置 SSO 登录的标志(用于退出登录时判断),然后跳转到系统首页即可。

// 集中平台单点登录
async ssoLogin(authCode) {
  const token = await apis.portal.ssoLogin({ authCode });
  this.afterSsoLogin(token);
},
async afterSsoLogin(token: string) {
  this.token = token;
  this.isSsoLogin = true;
  // 把sso登录状态保存到本地,防止用户刷新界面失效
  localMng.setItem(SsoLoginName, true);
  localMng.setItem(TokenName, token);
  request.setHeader({
    Authorization: token,
  });
  await Promise.all([this.getUserInfo(), this.getMenuList()]).then(()=>{
    router.replace("/home");
  }).catch(e =>{
    ElMessage.error(e);
  })
},

退出登录:

// 退出登录
const logout = () => {
  if(isSsoLogin.value) {
		appStore.logout();
		localMng.removeItem(SsoLoginName);
		window.close();
  } else {
    appStore.logout();
  }
}

于是,就完成了系统接入单点登录等前端适配工作,很简单吧。

参考链接

单点登录原理与实现