JS 沙箱实现方案

沙箱概述

在计算机安全中,Sandbox 是一种用于隔离正在运行程序的安全集资,通常用于执行未经测试或者不受信任的程序或者代码,它回为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响外部程序的执行。

js 沙箱的使用场景

  1. jsonp: 在解析服务器返回的jsonp数据的时候,如果不信任jsonp的数据,可以通过创建沙箱的方式来获取数据;执行第三方js(不受信任的js)的时候
  2. 在线代码编辑器: 在线编辑器中一般讲代码放在沙箱中执行,避免对页面本身造成影响
  3. vue 服务端渲染: vue 的服务端渲染, 通过创建沙箱执行前端的bundle文件;在调用createBundleRender方法的时候,允许配置runlnNewContext为true或false的形式,判断是否传入一个新创建的sandbox对象以供vm使用
  4. vue 模板中的表达式: vue 模板中表达式的计算被放在沙盒中,只能访问全局变量的一个白名单,如Math和Date.你不能在模板表达式中试图访问用户定义的全局变量

js 沙箱的实现方式

<一>、 基于iframe的沙箱环境实现

通过iframe来创建沙箱环境是一种常用的方法,iframe本身就是一个封闭的沙箱环境,假如即将执行的代码不是我们自己的代码,是不可信任的数据,那么可以通过iframe来执行

<!DOCTYPE html> 
<html lang="en">
	<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: 宽度 * name: 框架名称,window.frames[name]
  • scrolling: 框架是否滚动 * scr: 框架地址或者页面地址
  • srcdoc: 用于替换原来在HTML Body中的内容
  • sandbox: 对iframe进行一列限制

*同域情况下我们可以自由操作iframe和父框架的内容DOM,但是跨域条件下就只能进行页面跳转*

1.2.2 获取iframe 中的内容
  1. iframe.contentWindow: 获取iframe的window对象
  2. iframe.contentDocument: 获取iframe的document对象
1.2.3 在iframe中获取父级内容(同域)
  1. window.parent: 获取上一级的window对象,如果还是iframe则是改iframe的window对象
  2. window.top: 获取顶级容器的window对象
  3. window.self: 返回自身的window引用
1.2.4 sandbox
  1. 启用sandbox之后会对iframe页面进行一系列的限制:
  • script 脚本不能执行
  • 不能发送ajax请求
  • 不能使用本地存储,localstroage,cookie
  • 不能创建信的弹窗和window
  • 不能发送表单
  • 不能加载额外插件,flash等
<iframe sandbox src="..."></iframe> 
  1. 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实现微前端有哪些优点?

  1. 实现简单:子应用之间自带沙箱,天然隔离,互不影响
  2. 技术限制:可以各自使用完全不同的前端框架
  3. 消息传递: 只要每个iframe来自同一个源,就可以使用window.postMessageAPI进行消息传递

使用iframe实现微前端有哪些缺点

  1. Buddle的大小各异,构建时不能提取公共依赖关系
  2. 不支持SEO
  3. URL不同步: iframe页面url中的状态信息不能同步到父窗口,无法使用浏览器的前进和后退功能
  4. DOM结构不共享: iframe的页面布局只针对iframe窗口
  5. 全局上下问完全隔离,内存变量不共享
  6. 启动慢: 每次微应用进入都是一次浏览器上下问重建、资源重新加载的过程

<二>、基于Proxy的沙箱实现

基于沙箱实现沙箱的主要原理是,通过Proxy劫持沙箱全局window,记录对全局对象属性的更改,来修改window对象的属性和方法,在卸载和加载应用时关闭/激活沙箱,达到模拟沙箱的目的

<!DOCTYPE html>
<html lang="en">
<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 lang="en">

<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>
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值