沙箱概述
在計算機安全中,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>