沙箱概述
在计算机安全中,Sandbox 是一种用于隔离正在运行程序的安全集资,通常用于执行未经测试或者不受信任的程序或者代码,它回为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响外部程序的执行。
js 沙箱的使用场景
-
: 在解析服务器返回的jsonp数据的时候,如果不信任jsonp的数据,可以通过创建沙箱的方式来获取数据;执行第三方js(不受信任的js)的时候jsonp
-
: 在线编辑器中一般讲代码放在沙箱中执行,避免对页面本身造成影响在线代码编辑器
-
: vue 的服务端渲染, 通过创建沙箱执行前端的bundle文件;在调用createBundleRender方法的时候,允许配置runlnNewContext为true或false的形式,判断是否传入一个新创建的sandbox对象以供vm使用vue 服务端渲染
-
: vue 模板中表达式的计算被放在沙盒中,只能访问全局变量的一个白名单,如Math和Date.你不能在模板表达式中试图访问用户定义的全局变量vue 模板中的表达式
js 沙箱的实现方式
<一>、 基于iframe的沙箱环境实现
通过iframe来创建沙箱环境是一种常用的方法,iframe本身就是一个封闭的沙箱环境,假如即将执行的代码不是我们自己的代码,是不可信任的数据,那么可以通过iframe来执行
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
#root {
width: 800px;
height: 600px;
border: 2px solid blue;
}
</style>
<script>
window.onload = function() {
const parent = window;
const frame = document.createElement('iframe'); // 限制代码的执行能力
frame.sandbox = 'allow-same-origin';
const data = [1, 3, 5, 7, 9, 11, 13]
let newData = [] // 给当前页面发送消息
frame.onload = function(e) {
frame.contentWindow.postMessage(data)
}
// iframe 对接收到的数据进行处理
documnet.getElementById('root').appendChild(frame);
const code = ` return dataInIframe.filter(item => item % 3 === 0) `
frame.contentWindow.addEventListener('message', function(e) {
console.log('iframe send message:', e.data)
// 将计算结果发送给父页面
const func = new frame.contentWindow.Function('dataInIframe', code)
// 父页面接收到iframe传递的数据
parent.postMessage(func(e.data)); }) parent.addEventListener('message', function(e) {
console.log('parent - message from iframe', e.data);
}, false); } `
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
1.1使用iframe及自定义消息传递机制跨窗口通信
同源策略
会限制窗口之间进行通信:
* 如果我们对一个同源的窗口进行引用,是具有访问权限的
* 如果我们想对不同源的窗口进行访问,我们就没有权限访问这个窗口下的内容(变量、文档、上下文),但是location是例外的,通过location我们可以实现窗口间的跳转,但是我们无法获取到location的信息,无法看到用户当前所处的位置。
1.2 iframe简介
iframe 标签可以单独创建一个新的窗口,这个新的窗口有自己的document和window
1.2.1 基本属性
-
: 是否显示边框frameborder
-
: 框架作为一个普通元素的高度height
- *
: 宽度 *width
: 框架名称,window.frames[name]name
-
: 框架是否滚动 *scrolling
: 框架地址或者页面地址scr
-
: 用于替换原来在HTML Body中的内容srcdoc
-
: 对iframe进行一列限制sandbox
*同域情况下我们可以自由操作iframe和父框架的内容DOM,但是跨域条件下就只能进行页面跳转*
1.2.2 获取iframe 中的内容
- iframe.contentWindow: 获取iframe的window对象
- iframe.contentDocument: 获取iframe的document对象
1.2.3 在iframe中获取父级内容(同域)
- window.parent: 获取上一级的window对象,如果还是iframe则是改iframe的window对象
- window.top: 获取顶级容器的window对象
- window.self: 返回自身的window引用
1.2.4 sandbox
- 启用sandbox之后会对iframe页面进行一系列的限制:
- script 脚本不能执行
- 不能发送ajax请求
- 不能使用本地存储,localstroage,cookie
- 不能创建信的弹窗和window
- 不能发送表单
- 不能加载额外插件,flash等
<iframe sandbox src="..."></iframe>
- sandbox 常用配置:
- allow-forms: 允许提交表单
- allow-script: 允许运行脚本
- allow-same-origin: 允许同域请求,ajax,stroge
- allow-top-navigation: 允许iframe能够主导window.top进行页面跳转
- allow-popups: 允许iframe中弹出新窗口(window.open, target=“_blank”)
- allow-pointer-lock: 在iframe中可以锁定鼠标,主要和鼠标锁定有关
<iframe sandbox="allow-forms" src="..."></iframe>
使用iframe实现微前端有哪些优点?
- 实现简单:子应用之间自带沙箱,天然隔离,互不影响
- 技术限制:可以各自使用完全不同的前端框架
- 消息传递: 只要每个iframe来自同一个源,就可以使用window.postMessageAPI进行消息传递
使用iframe实现微前端有哪些缺点
- Buddle的大小各异,构建时不能提取公共依赖关系
- 不支持SEO
- URL不同步: iframe页面url中的状态信息不能同步到父窗口,无法使用浏览器的前进和后退功能
- DOM结构不共享: iframe的页面布局只针对iframe窗口
- 全局上下问完全隔离,内存变量不共享
- 启动慢: 每次微应用进入都是一次浏览器上下问重建、资源重新加载的过程
<二>、基于Proxy的沙箱实现
基于沙箱实现沙箱的主要原理是,通过Proxy劫持沙箱全局window,记录对全局对象属性的更改,来修改window对象的属性和方法,在卸载和加载应用时关闭/激活沙箱,达到模拟沙箱的目的
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>代理沙箱基本实现</title>
</head>
<body>
<h1>Proxy Sandbox</h2>
<script>
// 根据沙箱内对属性/方法的操作记录更该window对象的属性/方法
function updateWindowProps (prop, value, isDelete) {
if(value === undefined || isDelete){
// 删除属性
delete window[prop];
} else {
// 更新属性
window[prop] = value;
}
}
// 代理沙箱实现
class ProxySandbox {
constructor(name) {
// 代理沙箱名称
this.name = name;
// 沙箱全局对象
const sandboxWindow = Object.create(null);
// sandboxWindow 代理
this.proxy = null;
// 记录新增加的属性
this.addedPropsMap = new Map();
// 记录更新的属性
this.updatedPropsMap = new Map();
// 记录所有有更改记录的属性(新增/修改)
this.allChangedPropsMap = new Map()
const proxy = new Proxy(sandboxWindow, {
get(target, prop) {
return window[prop]
},
set(target, prop, value){
if(!window.hasOwnProperty(prop)) {
// window 对象上没有属性,记录新增
this.addedPropsMap.set(prop, value);
} else if(!this.updatedPropsMap.has(prop)) {
// window 对像上已经存在的值,但是还没有更新,记录更新
const orgVal = window[prop]
this.updatedPropsMap.set(prop, orgVal);
}
// 记录所有变更的对象
this.allChangedPropsMap.set(prop, value);
// 更改window对象
updateWindow(prop, value);
return true;
}
});
this.proxy = proxy
}
// 激活沙箱
activeSandbox() {
// 更新当前记录的所有属性
this.allChangedPropsMap.forEach((val, prop) => updateWindow(prop, val));
}
// 关闭沙箱
inactiveSandbox() {
// 还原所有更新过的属性
this.updatedPropsMap.forEach((val, prop) => updateWindow(prop, val));
// 删除所有沙箱内新添加的属性
this.addedPropsMap.forEach((_, prop) => updateWindow(prop, undefined, true))
}
}
// 代理沙箱测试
const sandbox = new ProxySandbox('代理沙箱')
const sandboxContext = sandbox.proxy
sandboxContext.dog = '旺财'
console.log('沙箱激活:', sandboxContext.dog, window.dog); // 旺财 旺财
//关闭沙箱
sandbox.inactiveSandbox();
console.log('关闭沙箱:', sandboxContext.dog, window.dog); // undefined undefined
// 重新激活沙箱
sandbox.activeSandbox();
console.log('沙箱激活:', sandboxContext.dog, window.dog); // 旺财 旺财
</script>
</body>
<三>、diff方式实现沙箱
在不支持proxy的浏览器中,可以通过diff的方式实现沙箱
- 在运行子应用时保存一个window的快照对象,将当前的window对象全部复制到这个快照对象中
- 子应用卸载时将window对象和快照对象对象进行diff,将不同的属性保存下来,再次挂载的时候再添加上这些属性
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>diff 实现沙箱</title>
</head>
<body>
<h1>Diff Sandbox</h1>
<script>
class DiffSandbox {
constructor(name) {
this.name = name
this.modifiedProps = {}
this.windowSnapshot = {}
}
activeSandbox() {
this.windowSnapshot = {}
for (let key in window) {
this.windowSnapshot[key] = window[key]
}
Object.keys(this.modifiedProps).forEach(propName => {
window[propName] = this.modifiedProps[propName]
})
}
inactiveSandbox() {
for (let key in window) {
if (this.windowSnapshot[key] !== window[key]) {
this.modifiedProps[key] = window[key]
window[key] = this.windowSnapshot[key]
}
}
}
}
// diff 沙箱测试
const diffSandbox = new DiffSandbox('diff沙箱')
// 激活沙箱
diffSandbox.activeSandbox()
window.cat = '1'
console.log('激活沙箱', window.cat) // 1
// 关闭沙箱
diffSandbox.inactiveSandbox()
console.log('关闭沙箱', window.cat) // undefined
diffSandbox.activeSandbox()
console.log('重新激活沙箱', window.cat) // 1
</script>
</body>
</html>