天天看點

JS 沙箱實作方案沙箱概述js 沙箱的使用場景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 >
	<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 >
<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>
           

繼續閱讀