先简单介绍两个概念:
(1)localStorage: 类似 sessionStorage,但其区别在于:存储在 localStorage 的数据可以长期保留;当页面被关闭时,存储在 sessionStorage 的数据会被清除。
(2)同源策略(same-origin policy):是浏览器执行的一种安全措施,目的是为了保证用户信息的安全,防止恶意的网站窃取数据。相关定义请参考developer.mozilla.org/zh-CN/docs/…
应用场景
一般用于业务域名众多,但是需要共享本地缓存数据。举个例子🌰,比如我用Chrome浏览天猫网站,在搜索栏搜索‘云安全’,过几天我用Chrome进了淘宝网,点击搜索栏想要获取到‘云安全’这个搜索记录,这个时候就需要用到localStroage的跨域存储(当然,也可以将搜索记录存储在服务端,这也是另一种解决办法)
实现原理
- 主要是通过postMessage来实现跨源通信
- 可以实现一个公共的iframe部署在某个域名中,作为共享域
- 将需要实现localStorage跨域通信的页面嵌入这个iframe
- 接入对应的SDK操作共享域,从而实现localStorage的跨域存储
具体实现
(注:以下代码均使用TypeScript编写)
1.共享域
CrossStorageHub
需要用到的数据结构
interface PermissionInfo {
/**
* 允许的源
* @example /\.example\.(?:com|net|cn)(?:\:\d+)?$/
*/
origin: RegExp;
/**
* 允许的请求方法
*/
allow: string[];
}
PermissionInfo主要用来配置域名的相关权限
interface ParamsInfo {
/**
* 键
*/
key: string;
/**
* set值
*/
value: string;
/**
* 批量del
*/
keys: string[];
}
interface RequestInfo {
/**
* 请求唯一标识
*/
id: number;
/**
* 请求方法
*/
method: 'get' | 'set' | 'del' | 'clear';
/**
* 请求参数
*/
params: ParamsInfo;
}
interface ResponseInfo {
/**
* 请求唯一标识
*/
id: number;
/**
* 错误信息
*/
error: string;
/**
* 请求结果
*/
result: string;
}
初始化,检测localStorage是否可用
public static init(permissions: PermissionInfo[]): void {
let available = true;
// 判断localStorage是否可用
if (!window.localStorage) available = false;
if (!available) {
window.parent.postMessage('unavailable', '*');
return;
}
this._permissions = permissions || [];
this._installListener();
window.parent.postMessage('ready', '*');
};
安装窗口消息事件所需的侦听器,兼容IE8及以上版本
private static _installListener(): void {
const listener = this._listener;
if (window.addEventListener) {
window.addEventListener('message', listener, false);
} else {
window['attachEvent']('onmessage', listener);
}
};
核心函数,处理来自父窗口的消息,忽略任何来源与权限不匹配的消息。根据method调用对应的函数('get' | 'set' | 'del' | 'clear'),响应请求的唯一标识,错误信息,以及result。
private static _listener(message: MessageEvent): void {
let origin: string, request: RequestInfo, method: string,
error: string, result: string[] | string, response: string;
origin = message.origin;
// 检查message.data是否为有效json
try {
request = JSON.parse(message.data);
} catch (err) {
return;
}
// 校验request.method数据类型
if (!request || typeof request.method !== 'string') {
return;
}
method = request.method;
if (!method) {
return;
} else if (!CrossStorageHub._permitted(origin, method)) {
error = 'Invalid permissions for ' + method;
} else {
try {
result = CrossStorageHub['_' + method](request.params);
} catch (err) {
error = err.message;
}
}
response = JSON.stringify({
id: request.id,
error: error,
result: result
})
window.parent.postMessage(response, origin);
};
为了防止恶意网站的接入,所以进行权限检测,不满足条件的消息将被过滤。
private static _permitted(origin: string, method: string): boolean {
let available: string[], match: boolean;
available = ['get', 'set', 'del', 'clear'];
if (!this._inArray(method, available)) {
return false;
}
for (const entry of this._permissions) {
if (!(entry.origin instanceof RegExp) || !(entry.allow instanceof Array)) {
continue;
}
match = entry.origin.test(origin);
if (match && this._inArray(method, entry.allow)) {
return true;
}
}
return false;
};
/**
* 取数据
* @param params
*/
private static _get(params: ParamsInfo): string {
const { key } = params;
return window.localStorage.getItem(key);
}
剩下的 set | del | clear 函数都是针对localStorage相关API的调用,我就不一一列举了
cross-storage-iframe.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="./bin/crossStorage/crossStorage.js" type="text/javascript"></script>
<script>
crossStorage.init([
{
origin: /\\*/,
allow: ['get', 'set', 'del']
}
]);
</script>
</body>
</html>
将代码编译成js文件进行引入。这里为了方便测试将origin配置为/\\*/,允许任何域名访问get,set,del方法。
2.父窗口CrossStorageSDK实现
主要是用来载入共享域并与之进行通信
sdk的初始化,加载cross-storage-iframe,以及监听message事件
class CrossStorageSDK {
private static readonly _storageSrc: string = "https://www.example.com/cross-storage-iframe.html";
private static readonly _storageOrign: string = "https://www.example.com";
private static _ready: boolean = false;
private static _callbacks: { [key: string]: (response?: ResponseInfo) => Promise<string> | void } = {};
public static async init(): Promise<void> {
return new Promise((resolve, reject) => {
this._installListener();
this._installCrossStorageIframe();
this._callbacks['ready'] = () => resolve();
this._callbacks['unavailable'] = () => reject();
})
}
/**
* 安装cross-storage-iframe
*/
private static _installCrossStorageIframe(): void {
const iframe = document.createElement('iframe');
iframe.frameBorder = iframe.width = iframe.height = '0';
iframe.name = 'cross-storage';
iframe.src = this._storageSrc;
document.body.appendChild(iframe);
}
/**
* 安装窗口消息事件所需的侦听器
* 兼容IE8及以上版本
*/
private static _installListener(): void {
const listener = this._listener;
if (window.addEventListener) {
window.addEventListener('message', listener, false);
} else {
window['attachEvent']('onmessage', listener);
}
};
}
处理来自cross-storage-iframe的事件消息,通过请求唯一标识进行回调。
private static _listener(message: MessageEvent): void {
let response: ResponseInfo;
// 忽略其他源发送的消息
if (message.origin !== CrossStorageSDK._storageOrign) return;
if (message.data === 'unavailable') {
CrossStorageSDK._callbacks['unavailable']();
return;
}
if (message.data === 'ready' && !CrossStorageSDK._ready) {
CrossStorageSDK._ready = true;
CrossStorageSDK._callbacks['ready']();
return;
}
// 检查message.data是否为有效json
try {
response = JSON.parse(message.data);
} catch (err) {
return;
}
CrossStorageSDK._callbacks[response.id] && CrossStorageSDK._callbacks[response.id](response)
};
对cross-storage-iframe的localStorage进行get操作。
public static get(key: string): Promise<string> {
return new Promise((resolve, reject) => {
const id = new Date().getTime();
const request: RequestInfo = {
id: id,
method: 'get',
params: {
key: key
}
}
this._callbacks[id] = (response: ResponseInfo) => {
response.error ? reject(response.error) : resolve(response.result);
}
this._sendRequest(request);
});
}
private static _sendRequest(request: RequestInfo): void {
window.frames[0].postMessage(JSON.stringify(request), this._storageOrign);
}
如何测试
a.html和b.html分别接入CrossStorageSDK,调用sdk所暴露的相关方法,就能实现localStorage的跨域(源)通信。
最后
Safari浏览器由于隐私限制,默认是禁用localStorage跨域存储的,需要用户主动开启设置。
如有遗漏或哪里写的不对,欢迎指正。
完整代码地址:github.com/leaveLi/cro…