昨天講 Chrome 92 起禁止 IFrame 內嵌跨網站網頁 alert/confirm/prompt 一事時提到,IFrame 內嵌跨網站網頁時原本就有諸多限制,基於安全考量,禁止被內嵌的第三方網頁存取母網頁,也不允許母網頁存取第三方網頁的 DOM、JavaScript 物件,甚至想看一下 location.href 也不准。

十年前我想出一種讓第三方網頁再 IFrame 內嵌母網頁同來源網頁的奇技淫巧,但隨時代演進,如果不必考慮 IE6/7/8/9/10,要與 IFrame 或 window.open() 開啟的跨網站網頁溝通有更方便的選擇 - window.postMessage() API。

postMessage() 允許兩個不同來源的 Window 物件(IFrame 內嵌或 window.open() 開啟)彼此溝通,提供指定及檢查對方來源的方法維護安全性。其原理是網頁可以呼叫第三方網頁 window 物件的 postMessage() 方法,傳入資料參數 data (可以是字串,也可以是任何 JavaScript 物件),並指定 targetOrigin (對方包含 Scheme、Host、Port 的 URL, 例如:ℎttp://child.utopia.net),瀏覽器會負責把關,確定對方是指定來源網站上的網頁才予以放行。(註:targetOrigin 輸入 "*" 代表不限定對象,但強烈不建議這麼做)

至於要接收資料的一方,則要實作 window 的 message 事件,事件參數物件包含 source 屬性可供檢核傳送資料來源(也是用 ℎttp://parent.utopia.net 這種 URL 形式),data 則為資料物件,另外有個 source 指向來源 Window 物件,可用呼叫對方 Window 物件的 postMessage() 回傳訊息,達成雙方溝通。

直接看範例比較好懂。

被內嵌的網頁 cross-origin.html 如下,在 window message 事件中,先檢查 e.orgin 排除不是來自 ℎttp://parent.utopia.net 的資料,取出 e.data 物件加以顯示,e.source 是來源視窗物件,反向呼叫它的 postMessage() 傳回 ACK 訊息。

<!DOCTYPE html>
<html>
<body>
	<div id=u style='font-size:10pt'></div>
	<script>
		document.getElementById('u').innerText = location.href;
	</script>
	<ul id=m></ul>
	<script>
	window.addEventListener('message', function(e) {
		//務必檢查呼叫來源,只開放特定網域呼叫,以免被惡意利用
		if (e.origin !== 'http://parent.utopia.net') 
			return;
		//傳送的資料物件
		let data = e.data;
		let mmss = data.time.toISOString().substr(14, 5);
		let li = document.createElement('li');
		li.innerText = mmss + ' ' + data.msg;
		document.getElementById('m').appendChild(li);
		//傳送 postMessage() 的來源網頁物件,可用來於向對方回傳 postMessage()
		let srcWin = e.source; 
		//回傳資料物件給送訊息的來源
		srcWin.postMessage('ACK for ' + mmss, e.origin);
	});
	</script>
</body>
</html>

parent.html 如下,有個按鈕觸發傳送訊息給 IFrame 內嵌網頁物件,postMessage() 傳入 { time: new Date(), msg: 'msg from parent'} 並指定對方必須來自 ℎttp://child.utopia.net;由於對方也會用 postMessage() 回傳,故 parent.html 也要用 addEventListener() 實作 message 事件接收 ACK 字串顯示出來。

<!DOCTYPE html>
<html>
<body>
	<div id=u style='font-size:9pt'></div>
	<script>
		document.getElementById('u').innerText = location.href;
	</script>
	<div style='padding: 6px'>
		<button id=b>postMessage()</button>
		<span id=s></span>
	</div>
	<iframe id=f src='http://child.utopia.net/aspnet/iframetest/cross-origin.html'></iframe>
	<script>
	document.getElementById('b').addEventListener('click', function() {
		document.getElementById('f').contentWindow.postMessage(
			//要傳遞的資料,可以是字串也可以是物件
			{ time: new Date(), msg: 'msg from parent'}, 
			//指定接受資料的對象,雖然可以用 * 全面開放,但不建議
			'http://child.utopia.net');
	});
	window.addEventListener('message', function(e) {
		//務必檢查呼叫來源,只處理來自特定網域的呼叫,以免被惡意利用
		if (e.origin !== 'http://child.utopia.net') 
			return;
		document.getElementById('s').innerText = e.data;
	});
	</script>
</body>
</html>

實際運作結果如下,輕鬆實現跨網站網頁溝通,很簡單吧?

Tips of how to use window.postMessage to pass data between cross-origin windows.


Comments

Be the first to post a comment

Post a comment