咱们是袋鼠云数栈 UED 团队,致力于打造优良的一站式数据中台产品。咱们始终保持工匠精力,摸索前端路线,为社区积攒并流传教训价值。
本文作者:空山
什么是沙箱
沙箱即 SandBox,它是一种平安机制,用于严格控制拜访资源。通过在程序中创立一个独立的运行环境,把一些起源不可信、具备破坏力或者又是无奈断定的恶意程序使其在该环境下运行,隔离了对外部程序的影响,这样即便产生了谬误或者平安问题都不会影响到里面。
咱们依据实现的计划不同, SandBox能够分为两种模式:
- 单实例模式:全局只存在一个实例,间接代理原生的
window
对象,记录每个沙箱内window
对象上的增删改等操作,激活某个沙箱时复原上一次失活时的状态,失活时复原原来window
的状态。 - 多实例模式:代理一个全新的
window
对象,所有的更改基于这个全新的对象,多个实例之间互不影响。
沙箱的利用场景
基于下面对沙箱的介绍,简而言之咱们最终的目标还是为了保障程序的失常运行,通过隔离的伎俩防止谬误、异样或者恶意代码的影响,在咱们日常开发或者接触中,也有很多这样的场景,以下列举几个:
- 微前端:微前端场景下,各个子利用被集成到一个运行时,防止每个子利用相互影响,导致的一些如全局净化的问题,上面会以
QianKun
为例进行具体的讲述 - JSONP:当运行通过
<script>
标签的url返回的JS代码时,为了躲避肯定水平上的危险可能须要在沙箱中执行 - 在线编辑器:在某些场景下咱们会提供一个编辑器或者相似的可输出界面须要用户自主的编辑代码,而后去执行它,比方:
CodeSandBox
对于用户输出的不确定代码为了避免净化最好是在沙箱中执行
咱们把它们进行形象的归类,大略能够分为以下三类:
- 执行时:执行不确定、不可信的JS代码
- 引入时:为引入的
JS
代码提供隔离环境 - 拜访时:执行代码对全局对象的拜访和批改进行限度
JS沙箱的常见解决方案
在实现JS
沙箱问题之前咱们须要有两点须要留神:
- 构建独立的上下文环境
- 模仿浏览器的原生对象
基于这两点,目前给出以下几种计划:
with
with
语句将扭转作用域
,会让外部的拜访优先从传入的对象上查找。怎么了解呢,咱们来看一下这一段代码:
const obj = { a: 1}const obj2 = { b: 2}const a = 9const fun = (obj) => { with(obj) { // 相当于{}内拜访的变量都会从obj上查找 console.log(a) a = 3 }}fun(obj) // 1console.log(obj) // { a: 3 }
在以后的外部环境中找不到某个变量时,会沿着作用作用域链一层层向上查找,如果找不到就拋出ReferenceError
异样。咱们看下上面这个例子:
const obj = { a: 1}const obj2 = { b: 2}const b = 9const fun = (obj) => { with(obj) { console.log(a, b) }}fun(obj) // 1 9fun(obj2) // ReferenceError: a is not defined
尽管with
实现了在以后上下文中查找变量的成果,然而依然存在一下问题:
- 找不到时会沿着作用域链往上查找
- 当批改存在的变量时,会同步批改外层的变量
除此之外with
还有其余的一些弊病具体理解
ES6 proxy
为了解决with
存在的问题,咱们来理解下proxy
办法。proxy
用于创立一个对象的代理,从而实现对基本操作的拦挡以及自定义。
根本语法
/*** @param {*} target - 应用Proxy包装的指标对象* @param {*} handler - 通常为一个函数,函数的各个属性别离定义了执行各个操作时的代理行为*/const p = new Proxy(target, handler)
具体理解
改良流程
code实现
const obj = { a: 1}const obj2 = { b: 2}let b = 3// 用with扭转作用域const withedCode = (code) => { // return (obj) => { // with(ctxProxy(obj)){ // eval(code) // } // } code = `with(obj) { ${ code } }` const fun = new Function('obj', code) return fun}// 执行代码const code = 'console.log(b)'// 白名单const whiteList = ['console', 'b']// // 拜访拦挡const ctxProxy = (ctx) => new Proxy(ctx, { has: (target, prop) => { // 当返回false的时候会沿着作用域向上查找,true为在以后作用域进行查找 if(whiteList.includes(prop)) { return false } if(!target.hasOwnProperty(prop)) { throw new Error(`can not find - ${prop}!`) } return true },})withedCode(code)(ctxProxy(obj2)) // 3
思考:为啥须要把console
增加到whiteList
中?
Tips:该案例在浏览器中运行失常,在node
中运行可能呈现问题,起因是应用了new Function
,创立的函数只能在全局作用域中运行,而在node
中顶级的作用域不是全局作用域,以后全局申明的变量是在以后模块的作用域里的。具体查看:Function
这样一个简略的沙箱是不是实现了,那咱们当初会不会想这样一个问题?
解决完对象的访问控制,咱们当初解决第二个问题如何模仿浏览器的全局对象———iframe
还有人不分明为啥须要模仿浏览器的对象吗?
- 一些全局对象办法的应用比方下面的
console.log
等 - 获取全局对象中的一些初始变量
with + proxy + iframe
咱们把原生浏览器的对象取出来
const iframe = document.createElement('iframe')document.body.append(iframe)const globalObj = iframe.contentWindow
创立一个全局代理对象的类
class GlobalProxy{ constructor(shareState) { return new Proxy(globalObj, { has: (target, prop) => { if(shareState.includes(prop)) { return false } if(!target.hasOwnProperty(prop)) { throw new Error(`can not find - ${prop}!`) } return true } }) }}
实际效果:
// 创立共享白名单const shareState = []// 创立一个沙箱实例const sandBox = new GlobalProxy(shareState)const withedCode = (code) => { code = `with(obj) { ${ code } }` const fun = new Function('obj', code) return fun}sandBox.abc = 123// 执行代码const code = 'console.log(abc)'withedCode(code)(sandBox)console.log(abc)//------console------// 123// undefined
Web Workers
通过创立一个独立的浏览器线程来达到隔离的目标,然而具备肯定的局限性
- 不能间接操作
DOM
节点 - 不能应用window
window
对象的默认办法和属性
......
起因在于workers
运行在另一个全局上下文中,不同于以后的window。
因而实用的场景大略相似于一些表达式的计算等。
通信形式
workers和主线程之间通过音讯机制进行数据传递
postMessage
——发送音讯onmessage
——解决音讯
咱们来简略看个例子:
// index.jswindow.app = '我是元数据'const myWorker = new Worker('worker.js')myWorker.onmessage = (oEvent) => { console.log('Worker said : ' + oEvent.data)}myWorker.postMessage('我是主线程!')// worker.jspostMessage('我是子线程!');onmessage = function (oEvent) { postMessage("Hi " + oEvent.data); console.log('window', window) console.log('DOM', document)}// -------------console-------------// Worker said : 我是子线程!// Worker said : Hi 我是主线程!// Uncaught ReferenceError: window is not defined
具体理解
沙箱逃逸
沙箱逃逸即通过各种伎俩解脱沙箱的解放,拜访沙箱外的全局变量甚至是篡改它们,实现一个沙箱还须要预防这些状况的产生。
Symbol.unscopables
Symbol.unscopables
设置了true
会对with
进行忽视,沿着作用域进行向上查找。
举个例子:
const obj = { a: 1}let a = 10obj[Symbol.unscopables] = { a: true}with(obj) { console.log(a) // 10}
改良上述with + proxy + iframe
中的全局代理对象的类
class GlobalProxy{ constructor(shareState) { return new Proxy(globalObj, { has: (target, prop) => { if(shareState.includes(prop)) { return false } if(!target.hasOwnProperty(prop)) { throw new Error(`can not find - ${prop}!`) } return true }, get: (target, prop) => { // 解决Symbol.unscopables逃逸 if(prop === Symbol.unscopables) return undefined return target[prop] } }) }}
window.parent
能够在沙箱的执行上下文中通过该办法拿到外层的全局对象
const iframe = document.createElement('iframe')document.body.append(iframe)const globalObj = iframe.contentWindowclass GlobalProxy{ constructor(shareState) { return new Proxy(globalObj, { has: (target, prop) => { if(shareState.includes(prop)) { return false } if(!target.hasOwnProperty(prop)) { throw new Error(`can not find - ${prop}!`) } return true }, get: (target, prop) => { // 解决Symbol.unscopables逃逸 if(prop === Symbol.unscopables) return undefined return target[prop] } }) }}// 创立共享白名单const shareState = []// 创立一个沙箱实例const sandBox = new GlobalProxy(shareState)const withedCode = (code) => { code = `with(obj) { ${ code } }` const fun = new Function('obj', code) return fun}sandBox.abc = 123sandBox.aaa = 789sandBox[Symbol.unscopables] = { aaa: true}var aaa = 123// 执行代码const code = 'console.log(parent.test = 789)'withedCode(code)(sandBox)console.log(window.test) // 789
改良计划
get: (target, prop) => { // 解决Symbol.unscopables逃逸 if(prop === Symbol.unscopables) return undefined // 阻止window.parent逃逸 if(prop === 'parent') { return target } return target[prop]}
原型链逃逸
通过某个变量的原型链向上查找,从而达到篡改全局对象的目标
const code = `([]).constructor.prototype.toString = () => { return 'Escape!'}`console.log([1,2,3].toString()) // Escape!
……
将来可尝试的新计划
ShadowRealms
它是将来JS的一项性能,目前曾经进入stage-3。通过它咱们能够创立一个独自的全局上下文环境来执行JS。
对于ShadowRealms的更多详情:
- https://fjolt.com/article/javascript-shadowrealms
- https://github.com/tc39/proposal-shadowrealm#APITypeScriptFormat
Portals
相似于iframe的新标签。
对于portals的更多详情
- https://github.com/WICG/portals/
探索QianKun中的沙箱
有了下面的常识储备当前,让咱们来看看QianKun
中的沙箱是怎么样子的,以下只讲述一些要害代码,源码地址:https://github.com/umijs/qiankun/tree/master/src/sandbox,版本为v2.6.3
咱们进入index文件看下
// 是否反对Proxy代理if (window.Proxy) { sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext);} else { sandbox = new SnapshotSandbox(appName);}
咱们能够看到QianKun
里的沙箱次要分为三种
LegacySandbox
:单实例代理沙箱,简略来讲就是只存在一个window实例,所有的操作都是对这一个实例的操作ProxySandbox
:多实例代理沙箱,通过对window的拷贝建设多个正本,在沙箱中对建设的正本进行操作SnapshotSandbox
:快照沙箱,基于 diff 形式实现的沙箱,用于不反对 Proxy 的低版本浏览器
SnapshotSandbox
咱们先来看下SnapshotSandbox这个沙箱,源码:
// 遍历对象function iter(obj: typeof window, callbackFn: (prop: any) => void) { for (const prop in obj) { if (obj.hasOwnProperty(prop) || prop === 'clearInterval') { callbackFn(prop); } }}/** * 基于 diff 形式实现的沙箱,用于不反对 Proxy 的低版本浏览器 */export default class SnapshotSandbox implements SandBox { proxy: WindowProxy; name: string; type: SandBoxType; sandboxRunning = true; private windowSnapshot!: Window; private modifyPropsMap: Record<any, any> = {}; constructor(name: string) { this.name = name; this.proxy = window; this.type = SandBoxType.Snapshot; } active() { // 记录以后快照 this.windowSnapshot = {} as Window; iter(window, (prop) => { this.windowSnapshot[prop] = window[prop]; }); // 复原之前的变更 Object.keys(this.modifyPropsMap).forEach((p: any) => { window[p] = this.modifyPropsMap[p]; }); this.sandboxRunning = true; } inactive() { this.modifyPropsMap = {}; iter(window, (prop) => { if (window[prop] !== this.windowSnapshot[prop]) { // 记录变更,复原环境 this.modifyPropsMap[prop] = window[prop]; window[prop] = this.windowSnapshot[prop]; } }); if (process.env.NODE_ENV === 'development') { console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap)); } this.sandboxRunning = false; }}
联合流程图
分块解读一下,这里咱们把它分成两块来看
激活时
active() { this.windowSnapshot = {} as Window; iter(window, (prop) => { // 通过遍历的形式记录以后window的状态,即window的以后快照 this.windowSnapshot[prop] = window[prop]; }); Object.keys(this.modifyPropsMap).forEach((p: any) => { // window[p] = this.modifyPropsMap[p]; // 通过遍历复原上一次失活时的变更 }); this.sandboxRunning = true;}
失活时
inactive() { this.modifyPropsMap = {}; iter(window, (prop) => { // 遍历window上的属性 if (window[prop] !== this.windowSnapshot[prop]) { this.modifyPropsMap[prop] = window[prop]; // 记录和快照不统一的属性到批改的对象中 window[prop] = this.windowSnapshot[prop]; // 复原window的属性为初始的属性 } }); this.sandboxRunning = false;}
SnapshotSandbox比较简单,因为不反对代理,所有的更改都在window上,只是在激活沙箱的时候保留一个window的初始快照,并在期间对变更的属性进行记录,失活时复原初始的window,然而会造成全局window的净化。
LegacySandbox
咱们来看下LegacySandbox沙箱,该沙箱基于Proxy实现的
流程图
源码局部
// 判断对象上的某个属性形容是否是可更改或者是可删除的function isPropConfigurable(target: WindowProxy, prop: PropertyKey) { const descriptor = Object.getOwnPropertyDescriptor(target, prop); return descriptor ? descriptor.configurable : true;}/** * 基于 Proxy 实现的沙箱 * TODO: 为了兼容性 singular 模式下仍旧应用该沙箱,等新沙箱稳固之后再切换 */export default class LegacySandbox implements SandBox { /** 沙箱期间新增的全局变量 */ private addedPropsMapInSandbox = new Map<PropertyKey, any>(); /** 沙箱期间更新的全局变量 */ private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>(); /** 继续记录更新的(新增和批改的)全局变量的 map,用于在任意时刻做 snapshot */ private currentUpdatedPropsValueMap = new Map<PropertyKey, any>(); name: string; proxy: WindowProxy; globalContext: typeof window; type: SandBoxType; sandboxRunning = true; latestSetProp: PropertyKey | null = null; // 设置globalContext对象上的属性 private setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) { if (value === undefined && toDelete) { delete (this.globalContext as any)[prop]; } else if (isPropConfigurable(this.globalContext, prop) && typeof prop !== 'symbol') { Object.defineProperty(this.globalContext, prop, { writable: true, configurable: true }); (this.globalContext as any)[prop] = value; } } active() { if (!this.sandboxRunning) { this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v)); } this.sandboxRunning = true; } inactive() { this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v)); this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true)); this.sandboxRunning = false; } constructor(name: string, globalContext = window) { this.name = name; this.globalContext = globalContext; this.type = SandBoxType.LegacyProxy; const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this; const rawWindow = globalContext; const fakeWindow = Object.create(null) as Window; const setTrap = (p: PropertyKey, value: any, originalValue: any, sync2Window = true) => { if (this.sandboxRunning) { if (!rawWindow.hasOwnProperty(p)) { addedPropsMapInSandbox.set(p, value); } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) { // 如果以后 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值 modifiedPropsOriginalValueMapInSandbox.set(p, originalValue); } currentUpdatedPropsValueMap.set(p, value); if (sync2Window) { // 必须从新设置 window 对象保障下次 get 时能拿到已更新的数据 (rawWindow as any)[p] = value; } this.latestSetProp = p; return true; } // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的状况下应该疏忽谬误 return true; }; const proxy = new Proxy(fakeWindow, { set: (_: Window, p: PropertyKey, value: any): boolean => { const originalValue = (rawWindow as any)[p]; return setTrap(p, value, originalValue, true); }, get(_: Window, p: PropertyKey): any { // avoid who using window.window or window.self to escape the sandbox environment to touch the really window // or use window.top to check if an iframe context // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13 if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') { return proxy; } const value = (rawWindow as any)[p]; return getTargetValue(rawWindow, value); }, // trap in operator // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12 has(_: Window, p: string | number | symbol): boolean { return p in rawWindow; }, getOwnPropertyDescriptor(_: Window, p: PropertyKey): PropertyDescriptor | undefined { const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p); // A property cannot be reported as non-configurable, if it does not exists as an own property of the target object if (descriptor && !descriptor.configurable) { descriptor.configurable = true; } return descriptor; }, defineProperty(_: Window, p: string | symbol, attributes: PropertyDescriptor): boolean { const originalValue = (rawWindow as any)[p]; const done = Reflect.defineProperty(rawWindow, p, attributes); const value = (rawWindow as any)[p]; setTrap(p, value, originalValue, false); return done; }, }); this.proxy = proxy; }}
同样的咱们来分块解读一下
这里有三个次要的变量,咱们须要先晓得下
/** 沙箱期间新增的全局变量 */private addedPropsMapInSandbox = new Map<PropertyKey, any>();/** 沙箱期间更新的全局变量,记录的是激活子利用时window上的初始值 */private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();/** 继续记录更新的(新增和批改的)全局变量的 map,用于在任意时刻做 snapshot */private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();// 白名单,是微利用之间全局共享的变量const variableWhiteList: PropertyKey[] = [ 'System', '__cjsWrapper', ...variableWhiteListInDev,]
激活时
active() { if (!this.sandboxRunning) { // 把上一次沙箱激活时的变更,设置到window上 this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v)); } this.sandboxRunning = true;}
失活时
inactive() { ... // 通过遍历还原window上的初始值 this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v)); // 通过对新增属的遍历,去除window上新增的属性 this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true)); this.sandboxRunning = false;}
Proxy代理
const proxy = new Proxy(fakeWindow, { set: (_: Window, p: PropertyKey, value: any): boolean => { const originalValue = (rawWindow as any)[p]; // 把变更的属性同步到addedPropsMapInSandbox、modifiedPropsOriginalValueMapInSandbox以及currentUpdatedPropsValueMap return setTrap(p, value, originalValue, true); }, get(_: Window, p: PropertyKey): any { // 避免通过应用top、parent、window、self拜访外层实在的环境 if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') { return proxy; } const value = (rawWindow as any)[p]; //一些异样解决获取value的实在值,次要解决了window.console、window.atob这类API在微利用中调用时会抛出 Illegal invocation异样的问题 return getTargetValue(rawWindow, value); }, has(_: Window, p: string | number | symbol): boolean { // 拜访的属性是否在rawWindow上,不在返回false沿着作用域向上查找 return p in rawWindow; }, // 拦挡对象的Object.getOwnPropertyDescriptor()操作 getOwnPropertyDescriptor(_: Window, p: PropertyKey): PropertyDescriptor | undefined { const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p); // A property cannot be reported as non-configurable, if it does not exists as an own property of the target object if (descriptor && !descriptor.configurable) { descriptor.configurable = true; } return descriptor; }, // 拦挡对象的Object.defineProperty()操作 defineProperty(_: Window, p: string | symbol, attributes: PropertyDescriptor): boolean { const originalValue = (rawWindow as any)[p]; const done = Reflect.defineProperty(rawWindow, p, attributes); const value = (rawWindow as any)[p]; setTrap(p, value, originalValue, false); // 变更属性记录 return done; },});const setTrap = (p: PropertyKey, value: any, originalValue: any, sync2Window = true) => { if (this.sandboxRunning) { // 判断是否为rawWindow本人的属性 if (!rawWindow.hasOwnProperty(p)) { // 新增的属性存入addedPropsMapInSandbox addedPropsMapInSandbox.set(p, value); } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) { // 批改的属性把初始值存入modifiedPropsOriginalValueMapInSandbox中 modifiedPropsOriginalValueMapInSandbox.set(p, originalValue); } // 以后数据的所有变更记录在currentUpdatedPropsValueMap中 currentUpdatedPropsValueMap.set(p, value); if (sync2Window) { // 必须从新设置 window 对象保障下次 get 时能拿到已更新的数据 (rawWindow as any)[p] = value; } this.latestSetProp = p; return true; } // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的状况下应该疏忽谬误 return true;}
依然是操作window对象,会造成全局window的净化,然而不须要记录window的初始快照,也不须要对window进行本身属性的整个遍历相比于diff快照效率会高点,性能会好点。
ProxySandbox
接下来咱们来看下ProxySandbox这个沙箱,该沙箱通过创立一个window的正本fakeWindow实现每个ProxySandbox实例之间属性互不影响
流程图
首先咱们须要创立一个window正本fakeWindow
和propertiesWithGetter
,后一个是用来记录有getter
且不可配置的Map对象,具体实现参考proxy
中的get局部
function createFakeWindow(globalContext: Window) { // 记录 window 对象上的 getter 属性,原生的有:window、document、location、top const propertiesWithGetter = new Map<PropertyKey, boolean>(); const fakeWindow = {} as FakeWindow; Object.getOwnPropertyNames(globalContext) // 遍历出window上所有不可配置的属性 .filter((p) => { const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); return !descriptor?.configurable; }) .forEach((p) => { // 获取属性描述符 const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); if (descriptor) { const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get'); if ( p === 'top' || p === 'parent' || p === 'self' || p === 'window' || (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop')) ) { descriptor.configurable = true; if (!hasGetter) { descriptor.writable = true; } } if (hasGetter) propertiesWithGetter.set(p, true); // 解冻某个属性,解冻当前该属性不可批改 rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor)); } }); return { fakeWindow, propertiesWithGetter, };}
ProxySandbox沙箱源码:
export default class ProxySandbox implements SandBox { /** window 值变更记录 */ private updatedValueSet = new Set<PropertyKey>(); name: string; type: SandBoxType; proxy: WindowProxy; globalContext: typeof window; sandboxRunning = true; latestSetProp: PropertyKey | null = null; private registerRunningApp(name: string, proxy: Window) { if (this.sandboxRunning) { const currentRunningApp = getCurrentRunningApp(); if (!currentRunningApp || currentRunningApp.name !== name) { setCurrentRunningApp({ name, window: proxy }); } // FIXME if you have any other good ideas // remove the mark in next tick, thus we can identify whether it in micro app or not // this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case nextTask(() => { setCurrentRunningApp(null); }); } } active() { if (!this.sandboxRunning) activeSandboxCount++; this.sandboxRunning = true; } inactive() { if (--activeSandboxCount === 0) { variableWhiteList.forEach((p) => { if (this.proxy.hasOwnProperty(p)) { // @ts-ignore delete this.globalContext[p]; } }); } this.sandboxRunning = false; } constructor(name: string, globalContext = window) { this.name = name; this.globalContext = globalContext; this.type = SandBoxType.Proxy; const { updatedValueSet } = this; const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext); const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>(); const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key); const proxy = new Proxy(fakeWindow, { set: (target: FakeWindow, p: PropertyKey, value: any): boolean => { if (this.sandboxRunning) { this.registerRunningApp(name, proxy); // We must kept its description while the property existed in globalContext before if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); const { writable, configurable, enumerable } = descriptor!; if (writable) { Object.defineProperty(target, p, { configurable, enumerable, writable, value, }); } } else { // @ts-ignore target[p] = value; } if (variableWhiteList.indexOf(p) !== -1) { // @ts-ignore globalContext[p] = value; } updatedValueSet.add(p); this.latestSetProp = p; return true; } // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的状况下应该疏忽谬误 return true; }, get: (target: FakeWindow, p: PropertyKey): any => { this.registerRunningApp(name, proxy); if (p === Symbol.unscopables) return unscopables; // avoid who using window.window or window.self to escape the sandbox environment to touch the really window // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13 if (p === 'window' || p === 'self') { return proxy; } // hijack globalWindow accessing with globalThis keyword if (p === 'globalThis') { return proxy; } if ( p === 'top' || p === 'parent' || (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop')) ) { // if your master app in an iframe context, allow these props escape the sandbox if (globalContext === globalContext.parent) { return proxy; } return (globalContext as any)[p]; } // proxy.hasOwnProperty would invoke getter firstly, then its value represented as globalContext.hasOwnProperty if (p === 'hasOwnProperty') { return hasOwnProperty; } if (p === 'document') { return document; } if (p === 'eval') { return eval; } const value = propertiesWithGetter.has(p) ? (globalContext as any)[p] : p in target ? (target as any)[p] : (globalContext as any)[p]; /* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation' See this code: const proxy = new Proxy(window, {}); const proxyFetch = fetch.bind(proxy); proxyFetch('https://qiankun.com'); */ const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext; return getTargetValue(boundTarget, value); }, // trap in operator // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12 has(target: FakeWindow, p: string | number | symbol): boolean { return p in unscopables || p in target || p in globalContext; }, getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined { /* as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object. */ if (target.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(target, p); descriptorTargetMap.set(p, 'target'); return descriptor; } if (globalContext.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); descriptorTargetMap.set(p, 'globalContext'); // A property cannot be reported as non-configurable, if it does not exists as an own property of the target object if (descriptor && !descriptor.configurable) { descriptor.configurable = true; } return descriptor; } return undefined; }, // trap to support iterator with sandbox ownKeys(target: FakeWindow): ArrayLike<string | symbol> { return uniq(Reflect.ownKeys(globalContext).concat(Reflect.ownKeys(target))); }, defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean { const from = descriptorTargetMap.get(p); /* Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p), otherwise it would cause a TypeError with illegal invocation. */ switch (from) { case 'globalContext': return Reflect.defineProperty(globalContext, p, attributes); default: return Reflect.defineProperty(target, p, attributes); } }, deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => { this.registerRunningApp(name, proxy); if (target.hasOwnProperty(p)) { // @ts-ignore delete target[p]; updatedValueSet.delete(p); return true; } return true; }, // makes sure `window instanceof Window` returns truthy in micro app getPrototypeOf() { return Reflect.getPrototypeOf(globalContext); }, }); this.proxy = proxy; activeSandboxCount++; }}
同样咱们把它分成几块来了解
激活时
active() { // 记录激活沙箱的数量 if (!this.sandboxRunning) activeSandboxCount++; this.sandboxRunning = true;}
失活时
inactive() { ... if (--activeSandboxCount === 0) { // variableWhiteList记录了白名单属性,须要在沙箱全副失活时进行属性的删除 variableWhiteList.forEach((p) => { if (this.proxy.hasOwnProperty(p)) { delete this.globalContext[p]; } }); } this.sandboxRunning = false;}
Proxy代理
set局部
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => { if (this.sandboxRunning) { // 记录以后运行的微利用 this.registerRunningApp(name, proxy); // 以后target不存在,然而globalContext中存在进行赋值 if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); const { writable, configurable, enumerable } = descriptor!; // 判断是否可写入,写入target即fakeWindow中 if (writable) { Object.defineProperty(target, p, { configurable, enumerable, writable, value, }); } } else { target[p] = value; } // 如果在白名单中间接在全局赋值 if (variableWhiteList.indexOf(p) !== -1) { globalContext[p] = value; } // 变更记录 updatedValueSet.add(p); this.latestSetProp = p; return true; } ... return true; },
get局部
get: (target: FakeWindow, p: PropertyKey): any => { this.registerRunningApp(name, proxy); // 避免逃逸,对不同状况进行解决 if (p === Symbol.unscopables) return unscopables; if (p === 'window' || p === 'self') { return proxy; } if (p === 'globalThis') { return proxy; } if ( p === 'top' || p === 'parent' || (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop')) ) { if (globalContext === globalContext.parent) { return proxy; } return (globalContext as any)[p]; } if (p === 'hasOwnProperty') { return hasOwnProperty; } // 间接返回document if (p === 'document') { return document; } //间接返回eval if (p === 'eval') { return eval; } // 参考https://github.com/umijs/qiankun/discussions/1411 const value = propertiesWithGetter.has(p) ? (globalContext as any)[p] : p in target ? (target as any)[p] : (globalContext as any)[p]; // 异样的解决,调用某些api的时候会呈现调用异样的状况 const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext; return getTargetValue(boundTarget, value); }
其余操作的一些兼容性解决,进一步保障了沙箱的平安
has(target: FakeWindow, p: string | number | symbol): boolean {...},getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {...},ownKeys(target: FakeWindow): ArrayLike<string | symbol> {...},defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {...},deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {...},getPrototypeOf() {...}
该模式最具劣势的一点是操作基于window上拷贝的正本FakeWindow,从而保障了多个沙箱实例并行的状况。
最初
欢送关注【袋鼠云数栈UED团队】\~\
袋鼠云数栈UED团队继续为宽广开发者分享技术成绩,相继参加开源了欢送star
- 大数据分布式任务调度零碎——Taier
- 轻量级的 Web IDE UI 框架——Molecule
- 针对大数据畛域的 SQL Parser 我的项目——dt-sql-parser
- 袋鼠云数栈前端团队代码评审工程实际文档——code-review-practices
- 一个速度更快、配置更灵便、应用更简略的模块打包器——ko