网页端SSO单点登录的实现

845 阅读6分钟

单点登录SSO(全称:Single Sign On),顾名思义就是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。传统的SSO的实现方式是cookie+session配合来实现,本文中主要向大家来介绍使用accessToken+localStorage来实现SSO的过程

应用场景

首先来介绍一下应用场景,账户登录授权系统(account.example.com)主要用来授权其它应用登录、维护账户信息等功能、系统A(a.example.com)和系统B(b.example.com)需要通过账户登录授权系统来完成授权登录,需要实现下面的几个功能:

  • 功能一:登录系统A之后,访问系统B时,系统B默认是已登录状态
  • 功能二:退出登录系统B之后,系统A也会随之退出登录
  • 功能三:当系统A/B需要修改登录用户资料时,会新创建一个Tab页并跳转到账户登录授权系统中进行修改,并且修改之后的用户信息需要同步到系统A/B

注:Web端授权登录的实现本篇博客不会涉及,会在下篇博客中进行详解

思路分析

应用在登录成功之后会将获取到的用户信息进行数据的持久化存储,防止用户在刷新页面之后丢失用户登录信息,从而让用户反复的进行登录。一般Web端的数据持久化是使用sessionStorage、localStorage或cookie,他们的优缺点如下:

  • sessionStorage由浏览器端生成,但仅在当前会话下有效,关闭页面或浏览器后被清除,存储大小为5M
  • localStorage由浏览器端生成,除非被主动清除,或卸载浏览器,否则永久保存,存储大小为5M
  • cookie一般由服务器生成,可设置失效时间。如果在浏览器端生成cookie,默认是关闭浏览器后失效,存储大小为4K

虽然上面三种数据持久化方法都能够满足我们的需求,但是它们有一个共同的限制就是无法进行跨域同步。举个栗子,系统A登录成功之后会将用户登录信息存储到a.example.com域下面的localStorage中,而系统B获取到的localStorage是b.example.com,因此要想实现单点登录和数据同步问题,就必须要解决localStorage的跨域数据同步。铺垫了这么多,终于要引出本篇博客的内容重点,就是localStorage的跨域同步。

localStorage的跨域同步

localStorage的跨域同步问题在Google上面搜索会找到很多介绍如何实现的博客,并且github也有很多解决localStorage跨域同步的开源项目,例如Star有1.9k的 cross-storagecross-domain-local-storage等,纵观这些解决方案,其大致思路就是通过在多个不同域的web页面中嵌入并加载一个共同域的iframe,利用iframe的window对象上的postMessage进行通讯,从而实现多个不同域的web页面的localStorage的跨域同步,图解如下:

cross-storage为例,实现了对共享localStorage的setItem、getItem、removeItem和clear操作,使用setItem和getItem操作可以实现应用场景中所提到的功能一,功能二和功能三却无法实现,因为现有的这些开源库没有提供监听localStorage中key,value变化的功能。因此想要实现对跨域localStorage的监听,需要我们自己来造一个功能更完善的轮子-cross-domain-shared-local-storage,实现思路参考了cross-storage并新增了监听数据变化的功能,下面我就来详细介绍下这个开源库。

示例

想要使用cross-domain-shared-local-storage实现跨域数据共享,首先我们需要理解两个重要的对象:CrossDomainStorageHubCrossDomainStorageClientCrossDomainStorageHub类似于C/S模式中的Server,它是iframe中需要初始化的对象,主要用来处理CrossDomainStorageClient中通过postMessage发送过来的请求;CrossDomainStorageClient从名字上来看就是C/S模式中的Client,主要是发送postMessage,向CrossDomainStorageHub发出指令。 首先我们来实现iframe中加载的html文件,我们暂定其名称为:hub.html,部署地址为:http://test.example.com/hub.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo</title>
</head>
<body>
    <script src="../lib/hub.min.js"></script>
    <script>
        CrossDomainStorageHub.getInstance().init([
            origin: "/\.example.com$/", // 允许通讯的域,正则对象
            allow: [ "get", "set", "del", "clear", "observed", "unobserved" ], // 允许origin所进行的操作
        ]);
    </script>
</body>
</html>

在hub.html中主要就是对StorageHub进行初始化设置,初始化参数详解如下:

  • origin 类型是RegExp,设置允许哪些域可以与其进行localStorage跨域通讯

  • allow 类型是string[], 可设置项有以下几种:

    • get 表示可以获取共享localStorage中的值
    • set 表示可以设置共享localStorage中的值
    • del 表示可以删除共享localStorage中的值
    • clear 表示可以清除掉共享localStorage中的所有值
    • observed 表示可以订阅共享localStorage中值变化的监听
    • unobserved 表示可以取消订阅共享localStorage中值变化的监听

    接下来我们来实现 a.example.com 中的Client部分

    import { CrossDomainStorageClient, IStorageChange } from 'cross-domain-shared-storage';
    
    // 建立连接并获得client对象
    const client = await CrossDomainStorageClient.getInstance().connect('http://test.example.com/hub.html', { timeout: 5000 });
    
    // 注册监听共享localStorage中的key为curUser值的变化
    client.subscribeItems([ 'curUser' ], (ev: IStorageChange) =>
    {
      const { newValue, oldValue } = ev;
      
      if(newValue !== oldValue && !oldValue) // 表示用户从未登录状态到登录成功状态
      {
           // 进行用户登录成功之后的相关操作
      }
      else if(newValue !== oldValue && oldValue) // 表示当前登录用户信息被修改
      {
          // 将修改后的用户信息同步到StateTree
      }
      else if(newValue !== oldValue && !newValue) // 表示当前登录用户退出登录
      {
          // 进行用户退出登录的相关操作
      }
    });
    
    // 设置用户信息
    const result = await client.setItem('curUser', curUserJsonStr);
    
    // 获取当前用户信息
    const curUserJsonStr = await client.getItem('curUser');
    
    // 清空当前用户信息
    const result = await client.removeItem('curUser');
    

    API

    CrossDomainStorageHub.prototype.init(permissions)

    初始化CrossDomainStorageHub对象方法,接受带有key为origin和allow的权限对象数组,origin的类型应为RegExp,allow为字符串数组,具体作用上面有提到,这里不再赘述

    CrossDomainStorageClient.prototype.connect(url, [opts])

    建立与iframe中的CrossDomainStorageHub对象的通讯,url为iframe所加载的网址,另外还接受一个options对象,在这个对象中可以设置timeout超时时间(默认超时时间为5000ms),最终返回一个由Promise包裹的CrossDomainStorageClient实例对象

    CrossDomainStorageClient.prototype.setItem(key, value)

    向跨域共享localStorage中写入指定键的值,key和value都是string类型,最终返回Promise<boolean>

    CrossDomainStorageClient.prototype.getItem(key)

    获取跨域共享localStorage中指定key的值,返回Promise<string>

    CrossDomainStorageClient.prototype.removeItem(key)

    清除跨域共享localStorage中指定key的值,返回Promise<boolean>

    CrossDomainStorageClient.prototype.clear()

    清除跨域共享localStorage中所有的键值对,返回Promise<boolean>

    CrossDomainStorageClient.prototype.subscribeItems(keys, callback)

    订阅跨域共享localStorage中指定key的value变化的监听,keys是要监听key的数组集合

    CrossDomainStorageClient.prototype.unsubscribeItems()

    取消订阅跨域共享localStorage中value变化的监听