单点登录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-storage,cross-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实现跨域数据共享,首先我们需要理解两个重要的对象:CrossDomainStorageHub 和 CrossDomainStorageClient;CrossDomainStorageHub类似于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变化的监听