介紹當碰到內崁 iframe 的網頁與父頁面不同網域時,該如何使用跨網域的方式傳遞訊息。
前言
一般來說在兩個不同網域下的網站基於安全性考量使用 JavaScript 是無法直接互相存取的,通常會碰到需要能夠存取的情況就是父網頁包了一個 iframe 的子網頁,而子網頁在做了某件事情後需要改變父網頁的某個元素值,而當父與子頁面網域不一樣時就需要進行跨網域的處理,在 IE 8 以上、Firefox、Chrome 可以使用 HTM5 的 postMessage 方法處理,而 IE 6、IE 7 就必須使用一些 hack 的技巧來做,以下介紹 3 種跨網域呼叫的處理方式。
環境建置
為了進行跨網域呼叫的測試,首先來進行環境的建置,當網站的 Domain 或 Port 不一樣時即識別為兩個不同的網域,為了在本機進行測試我們需先模擬出兩個不同的網域。
建立測試網站
首先開啟 VS 加入兩個新網站 Doman1 、Domain2,在 Domain1 加入一個父網頁與一個子網頁做為同網域測試用,另外在 Domain2 加入一個子網頁做為跨網域測試用,如下。
修改 Hosts 檔案
透過修改本機 Hosts 檔案,我們可以讓兩個自定義的 Domain 於本機 IIS 下可以識別,找到「C:\Windows\System32\drivers\etc」路徑中的 Hosts 檔案,將檔案移至桌面後開啟,於內容最下方增加以下兩個對應,如下,完成後將檔案移動回目錄中。
#For NC, no new entry above, between this and next comments
172.19.2.250 bifrost.vwparty.com
#end of NC entry
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host
# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
127.0.0.1 www.test1-domain.com
127.0.0.1 www.test2-domain.com
架設網站
網站建立與 Hosts 設定好後就可以在 IIS 上建立兩個網站用來進行測試,參考下圖將兩個網站建立於 IIS 。
完成後執行 www.test1-domain.com 網址,就會看到下圖的測試頁。
加入測試代碼
測試環境完成後看到 Domain1 網站的 Default.aspx 網頁,此頁為父頁面而其中的內容如下。
<div>
<h2>This domain: http://www.test1-domain.com/</h2>
Result:
<ul id="result"></ul><br />
Same domain iframe:<br />
<iframe src="SameDoamin.aspx"
style="border: 1px solid #000; width: 400px; height: 100px"></iframe>
<br />
<br />
Cross domain iframe:<br />
<iframe src="http://www.test2-domain.com/CrossDomain.aspx"
style="border: 1px solid #000; width: 400px; height: 100px"></iframe>
<script type="text/javascript">
function ReceiveCallback(msg) {
var li = document.createElement("li");
li.innerHTML = msg;
document.getElementById("result").appendChild(li);
}
</script>
</div>
可以看到在父頁面中崁入了兩個 iframe 分別連到不同網址,第一個 iframe 連結的是相同網域的子網頁,第二個 iframe 連結的是另一個網域的子網頁,另外可以看到加入了 ReceiveCallback 方法要讓子網頁進行呼叫,而另外還需在子網頁中加入以下 Script ,在子網頁點擊按鈕後直接呼叫 SendMessage 方法去呼叫父網頁的 ReceiveCallback 方法,如下。
Domain1 SameDomain 頁面
<div>
<h5>This frame domain: http://www.test1-domain.com/</h5>
<p>
<input id="Button1" type="button"
value="Use top.method()" onclick="SendMessage()" /></p>
<script type="text/javascript">
// 直接叫用父視窗的方法
function SendMessage() {
top.ReceiveCallback("From same domain callback.");
}
</script>
</div>
Domain2 CrossDomain 頁面
<div>
<h5>
This frame domain: http://www.test2-domain.com/</h5>
<p>
<input id="Button1" type="button"
value="Use top.method()" onclick="SendMessage()" />
</p>
<script type="text/javascript">
// 直接叫用父視窗的方法
function SendMessage() {
try {
top.ReceiveCallback("From cross domain callback.");
} catch (e) {
alert(e);
}
}
</script>
</div>
接著直接執行網頁進行測試,如下。
在此可以發現,當使用同網域的子網頁呼叫父網頁方法時並不會有問題,但是使用其他網域的子網頁呼叫父網頁方法時就發生沒有權限的錯誤,這就是因為安全性的問題導致跨網域時無法存取父頁面,而為了處理跨網頁的問題,接下來將使用三種方式讓不同網域的子網頁能夠去呼叫父網頁的方法。
方法一、使用 HTML5 postMessage() 與 onMessage
在 HTML5 對於不同網頁傳遞訊息的處理提供了 postMessage 方法,在使用 postMessage / onMessage 的情況下在發送訊息的網頁需要使用 postMessage() 方法來發送訊息,而接收訊息的網頁需要將 onMessage 事件加入監聽,注意此方式只適用於 IE 8 以上、Firefox 6.0 以上、Chrome 1.0 以上、Safari 4.0 以上、Opera 9.5 以上,實際做法 參考以下。
接收頁加入 onMessage 事件監聽
在接收訊息的頁面首先將 onMessage 加入監聽,加入監聽可以使用 windows.addEventListener() 方法,要注意的是 IE 加入監聽的事件名稱不需要加 on ,將監聽事件綁定在 ReceiveCallback 方法上。
<div>
<h2>This domain: http://www.test1-domain.com/</h2>
Result:
<ul id="result"></ul><br />
Same domain iframe:<br />
<iframe id="ifrm1" src="SameDomain2.aspx"
style="border: 1px solid #000; width: 400px; height: 100px"></iframe>
<br />
<br />
Cross domain iframe:<br />
<iframe id="ifrm2" src="http://www.test2-domain.com/CrossDomain2.aspx"
style="border: 1px solid #000; width: 400px; height: 100px"></iframe>
<script type="text/javascript">
/*
postMessage / onMessage 方式
*/
if (window.addEventListener) {
window.addEventListener('message', ReceiveCallback, false);
} else if (window.attachEvent) {
window.attachEvent('onmessage', ReceiveCallback);
}
function ReceiveCallback(msg) {
if (msg.origin == "http://www.test1-domain.com") {
var li = document.createElement("li");
li.innerHTML = msg.data;
document.getElementById("result").appendChild(li);
}
}
</script>
</div>
發送頁使用 postMessage() 方法傳遞訊息
在發送訊息的頁面需要使用 postMessage() 方法將訊息傳遞回父網頁,第一個參數為訊息內容而第二個參數為 Origin,如下。
<div>
<h5>
This frame domain: http://www.test2-domain.com/</h5>
<p>
<input id="Button1" type="button"
value="Use postMessage()" onclick="SendMessage()" /></p>
<script type="text/javascript">
// postMessage
function SendMessage() {
parent.postMessage("From cross domain callback.",
"http://www.test1-domain.com");
}
</script>
</div>
以上代碼設置好後即可進行測試,如下。
方法二、透過同網域 proxy 呼叫父頁面方法
為了要讓不同網域的子頁面可以存取父網頁,我們可以透過在不同網域的子頁面再崁入一個由父網頁提供的 Proxy 頁面,透過此 Proxy 頁面去呼叫同網域的父頁面 ReceiveCallback 方法,如下。
增加 Proxy 頁面
首先於父頁面網站中增加一個 Proxy 頁面用來讓跨網域的網站引用呼叫父頁面使用,如下
<!DOCTYPE html>
<html>
<head>
<title>proxy</title>
<script type="text/javascript">
// 取得網址參數
var qryString = window.location.href.split("?")[1] || false;
if (qryString && qryString.indexOf("msg=") != -1) {
// 去除參數名稱
var param = qryString.replace("msg=", "");
// 呼叫父頁面方法
top.ReceiveCallback(param);
}
</script>
</head>
<body>
</body>
</html>
傳入父頁面網址
修改父頁面代碼,多增加一個參數將父頁面的網址當作參數入不同網域的子頁面,如下。
<script type="text/javascript">
/*
透過同網域 proxy 呼叫父頁面方法
*/
window.onload = function () {
var crossiframe = document.getElementById("ifrm2");
// 將父頁面網址當作參數傳入子頁面
crossiframe.src = crossiframe.src + "?root=" + encodeURI(window.location.host);
};
function ReceiveCallback(msg) {
var li = document.createElement("li");
li.innerHTML = msg;
document.getElementById("result").appendChild(li);
}
</script>
接收父頁面網址並串接 Proxy
修改不同網域子頁面代碼,修改按鈕呼叫 SendMessage 方法,該方法主要動作為取得父頁面網址後串接 Proxy 頁面並帶入需要傳入的參數,再動態產生 iframe 指向該 Proxy 網址,透過 Proxy 頁面去呼叫父頁面的 ReceiveCallback 方法,如下。
<script type="text/javascript">
function SendMessage() {
if (CheckIsSameDomain()) {
top.ReceiveCallback("From same domain callback.");
}
else {
UseProxy("From cross domain callback.");
}
}
function CheckIsSameDomain() {
try {
return !!top.location.href;
} catch (e) {
return false;
}
}
function UseProxy(msg) {
var rootUrl = "";
// 取得參數
var qryString = window.location.href.split("?")[1] || false;
if (qryString && qryString.indexOf("root=") != -1) {
rootUrl = qryString.replace("root=", "");
// 串接 proxy 網址
rootUrl = "http://" + rootUrl + "\/proxy.html?msg=" + msg;
}
if (rootUrl.length > 0) {
var iframe = document.createElement("iframe");
iframe.src = rootUrl;
iframe.style.display = "none";
document.body.appendChild(iframe);
}
}
</script>
現在透過了 Proxy 此頁面的中介處理,即可以將需要傳輸的資料傳入傳送到父頁面處理,測試如下。
請注意!以上為簡單測試並未針對 XSS 做特別處理,另外也可以參考此篇文章的處理方法 IFrames and cross-domain security, part 3 。
方法三、使用 easyXDM 函式庫進行跨網域 (推薦)
easyXDM 為 JavaScript 的跨網域處理的函式庫,此函式庫提供了多種方法的跨網域雙向溝通,所以比較建議直接使用此函式庫即可,但在 IE 6、7 的瀏覽器必須額外使用 easyxdm.swf 與 name.html 這兩個檔案,所以瀏覽器需要安裝 Flash,首先至 http://easyxdm.net/ 網站下載該函式庫的檔案,下載完成後放入 Domain1 與 Domain2 網站中,如下。
於 Domain1 建立 RPC
在 Domain1 網站的父頁面中,首先需要建立一個 RPC 類,參考以下代碼。
<div>
<h2>This domain: http://www.test1-domain.com/</h2>
Result:
<ul id="result"></ul><br />
Same domain iframe:<br />
<div id="ifrm1"></div>
<br />
<br />
Cross domain iframe:<br />
<div id="ifrm2"></div>
<script type="text/javascript">
/*
使用 easyXDM 函式庫方式
*/
function ReceiveCallback(msg) {
var li = document.createElement("li");
li.innerHTML = msg;
document.getElementById("result").appendChild(li);
}
// RPC
var rpcLocal = new easyXDM.Rpc({
local: "/easyXDM/name.html",
swf: "/easyXDM/easyxdm.swf",
remote: "/SameDomain4.aspx",
remoteHelper: "/easyXDM/name.html",
container: "ifrm1",
props: {
style: {
border: "1px solid #000",
height: "100px",
width: "400px"
}
}
}, {
local: {
RemoteCallback: function (msg) {
ReceiveCallback(msg);
}
}
});
var rpcRemote = new easyXDM.Rpc({
local: "/easyXDM/name.html",
swf: "http://www.test2-domain.com/easyXDM/easyxdm.swf",
remote: "http://www.test2-domain.com/CrossDomain4.aspx",
remoteHelper: "http://www.test2-domain.com/easyXDM/name.html",
container: "ifrm2",
props: {
style: {
border: "1px solid #000",
height: "100px",
width: "400px"
}
}
}, {
local: {
RemoteCallback: function (msg) {
ReceiveCallback(msg);
}
}
});
</script>
</div>
在設定部分
- local : 本地的 name.html 檔案路徑 (IE 6、7 需要設定)
- swf : 遠端的 easyxdm.swf 檔案路徑 (IE 6、7 需要設定)
- remote : 遠端的網址路徑
- remoteHelper : 遠端的 name.html 檔案路徑 (IE 6、7 需要設定)
- container : 動態加載的目標 iframe tag 名稱
- props : 動態加載的目標 iframe 樣式
在 local 這邊則是指定你要公開可呼叫的方法,之後子頁面將可直接呼叫此公開的方法,多個請用 "," 號分隔。
於 Domain2 建立 RPC
在 Domain2 網站的子頁面中,也需要建立 RPC,參考以下代碼。
<div>
<h5>
This frame domain: http://www.test2-domain.com/</h5>
<p>
<input id="Button1" type="button"
value="Use easyXDM RPC" onclick="remote.RemoteCallback('From cross domain callback.')" />
</p>
<script type="text/javascript">
// RPC
var remote = new easyXDM.Rpc({
local: "/easyXDM/name.html",
swf: "/easyXDM/easyxdm.swf"
}, {
remote: {
RemoteCallback: {}
}
});
</script>
</div>
在設定部分
- local : 本地的 name.html 檔案路徑 (IE 6、7 需要設定)
- swf : 本地的 easyxdm.swf 檔案路徑 (IE 6、7 需要設定)
而在 remote 部分,在此需要宣告父頁面的公開方法 Interface ,子頁面透過 Interface 呼叫父頁面的公開方法,多個請用 "," 號分隔。
另外 easyXDM 函式庫還有其他的使用方式,詳細的使用方式可以參考官方網站的文件庫,實際執行結果如下。
看過以上三種處理方式後對於跨網域呼叫的處理,參考以上的三種方式就能夠讓內崁 iframe 的功能可以在父與子頁面互相傳遞訊息了。
範例程式碼
參考資料
iFrames and cross-domain security
IFrames and cross-domain security, part 2
IFrames and cross-domain security, part 3
以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)