這次全了,8 種超詳細 Web 跨域解決方案!
在日常開發過程中,我們通常都會遇到 ajax 跨域請求或者前端跨域通信的開發場景。無論是前端同學還是後端同學都需要具備解決跨域問題的能力。本文總結梳理了常見的跨域場景、跨域解決方案及其優缺點,希望可以作爲大家解決跨域問題的參考。
一、什麼是跨域
當 a.qq.com 域名下的頁面或腳本試圖去請求 b.qq.com 域名下的資源時,就是典型的跨域行爲。跨域的定義從受限範圍可以分爲兩種,廣義跨域和狹義跨域。
(一)廣義跨域
廣義跨域通常包含以下三種行爲:
-
資源跳轉:a 鏈接、重定向。
-
資源嵌入:、、、 等 dom 標籤,還有樣式中 background:url()、@font-face() 等文件外鏈。
-
腳本請求:瀏覽器存儲數據讀取、dom 和 js 對象的跨域操作、js 發起的 ajax 請求等。
其中,資源跳轉和資源嵌入行爲可以正常請求到跨域資源,腳本請求在未經任何處理的情況下,通常會有跨域問題。
(二)同源策略
同源策略(Same origin policy, SOP)是一種約定,由 Netscape 公司 1995 年引入瀏覽器,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,瀏覽器很容易受到 XSS、CSFR 等攻擊。所謂同源策略是指 “協議 + 域名 + 端口” 三者相同。
(三)狹義跨域
狹義跨域正是瀏覽器同源策略限制的一類請求場景,即我們通常說的跨域行爲,通常包含以下三種行爲:
-
cookie、localStorage 和 indexDB 無法讀取。
-
dom 和 js 對象無法獲取和操作。
-
ajax 請求無法發送。
二、常見跨域場景
三、跨域解決方案
(一)ajax 跨域請求解決方案
日常開發過程中,絕大多數前端頁面都會向後端發送 ajax 請求進行數據交互。那麼,ajax 請求遇到跨域問題,如何進行解決呢。本文總結以下四種常見解決方案:
-
jsonp 跨域
jsonp (JSON with Padding),是 JSON 的一種 “使用模式”,可以讓網頁跨域讀取數據。其本質是利用 script 標籤的開放策略,瀏覽器傳遞 callback 參數到後端,後端返回數據時會將 callback 參數作爲函數名來包裹數據,從而瀏覽器就可以跨域請求數據並定製函數來自動處理返回數據。
jsonp 跨解實現流程:
jsonp 跨域代碼示例:
var script = document.createElement('script');
script.type = 'text/javascript';
// 傳參callback給後端,後端返回時執行這個在前端定義的回調函數
script.src = 'http://a.qq.com/index.php?callback=handleCallback';
document.head.appendChild(script);
// 回調執行函數
function handleCallback(res) {
alert(JSON.stringify(res));
}
jsonp 跨域優點:
- jsonp 兼容性強,適用於所有瀏覽器,尤其是 IE10 及以下瀏覽器。
jsonp 跨域缺點:
-
沒有關於調用錯誤的處理。
-
只支持 GET 請求,不支持 POST 請求以及大數據量的請求,而且也無法拿到相關的返回頭,狀態碼等數據。
-
callback 參數惡意注入,可能會造成 xss 漏洞。
-
無法設置資源訪問授權。
-
跨域資源共享(CORS)
跨域資源共享(Cross-origin resource sharing,CORS)是一個 W3C 標準,允許瀏覽器向跨域服務器發送請求,從而克服了 ajax 只能同源使用的限制。CORS 需要瀏覽器和服務器同時支持。目前,所有主流瀏覽器(IE10 及以上)使用 XMLHttpRequest 對象都可支持該功能,IE8 和 IE9 需要使用 XDomainRequest 對象進行兼容。
CORS 整個通信過程都是瀏覽器自動完成,瀏覽器一旦發現 ajax 請求跨源,就會自動在頭信息中增加 Origin 字段,用來說明本次請求來自哪個源(協議 + 域名 + 端口)。因此,實現 CORS 通信的關鍵是服務器,需要服務器配置 Access-Control-Allow-Origin 頭信息。當 CORS 請求需要攜帶 cookie 時,需要服務端配置 Access-Control-Allow-Credentials 頭信息,前端也需要設置 withCredentials。
瀏覽器將 CORS 請求分成兩類:簡單請求和非簡單請求。簡單請求需要滿足以下兩大條件:
-
請求方法是以下三種方法之一:HEAD、GET、POST。
-
HTTP 的頭信息不超出以下幾種字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type:只限於三個值 application/x-www-form-urlencoded、multipart/form-data、text/plain。
CORS 簡單請求跨域實現流程:
CORS 簡單請求跨域代碼示例:
// IE8/9需用XDomainRequest兼容
var xhr = new XMLHttpRequest();
// 前端設置是否帶cookie
xhr.withCredentials = true;
xhr.open('post', 'http://a.qq.com/index.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=saramliu');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
CORS 跨域優點:
-
支持所有類型的 HTTP 請求,功能完善。
-
通過 onerror 事件監聽進行調用錯誤處理;
-
通過 Access-Control-Allow-Origin 進行資源訪問授權。
CORS 跨域缺點:
-
目前主流瀏覽器(IE10 及以上)都支持 CORS,但 IE8 和 IE9 需要使用 XDomainRequest 對象進行兼容,IE7 及以下瀏覽器不支持。
-
Flash 跨域(僅供 IE7 及以下瀏覽器參考使用)
由於 IE7 及以下瀏覽器默認是不兼容跨域請求的,那麼在不改造後端的情況下,可以考慮使用 Flash 進行跨域請求。
Flash 在進行跨域請求時,默認首先會發送預檢請求,檢查服務器域名根目錄下的 crossdomain.xml 文件,判斷請求域是否合法。如果域名不合法,Flash 直接封殺請求;如果域名合法,則發送真實請求,獲取數據並返回給前端頁面。
corssdomain.xml 是目標域下的主策略文件,其文件配置規則如下:
(1)cross-domain-policy
crossdomain.xml 的根元素,包含以下子元素:
-
site-control
-
allow-access-from
-
allow-access-from-identity
-
allow-http-request-headers-from
(2)site-control
是否允許加載其他策略文件,屬性值 permitted-cross-domain-policies 允許的值有:
-
none,不允許加載策略文件(包括該主策略文件)。
-
master-only,僅允許加載主策略文件。
-
by-content-type,僅允許加載 HTTP/HTTPS 協議下 Content-Type 爲 text/x-cross-domain-policy 的策略文件。
-
by-ftp-filename,僅允許加載 FTP 協議下文件名爲 crossdomain.xml 的策略文件。
-
all,允許加載任何策略文件。
(3)allow-access-from
用於授權數據訪問的請求域,具有以下屬性:
-
domain,指定授予權限的域,可以是域名或 IP 地址。
-
to-ports,指定授予權限的 socket 連接端口範圍,以逗號分隔的端口列表,端口範圍通過在兩個端口號之間插入短劃線 - 指定。
-
secure,指定信息是否經加密傳輸。
(4)allow-access-from-identity
允許有特定證書的請求域跨域訪問目標域上的資源。
(5)allow-http-request-headers-from
用於授權發送用戶定義 HTTP 頭的請求域,具有以下屬性:
-
domain,指定授予權限的域,可以是域名或 IP 地址。
-
headers,表明允許發送的 HTTP 頭,以逗號分隔。
-
secure:指明信息是否經加密傳輸。
crossdomain.xml 文件配置示例:
<cross-domain-policy>
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*.qq.com"/>
<allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>
Flash 跨域實現流程:
Flash 跨域代碼示例:
var FlashHelper = new Object();
// 寫入Flash對象
FlashHelper.writeFlash = function () {
var swfName = "Flash4AJAX.swf";
div = document.createElement("div");
div.style.height = '0';
div.innerHTML = '<object data="' + swfName + '"'
+ ' type="application/x-shockwave-flash"'
+ ' height="0" width="215">'
+ '<param >'
+ '<param >'
+ '<param >'
+ '<\/object>';
document.body.appendChild(div);
}
FlashHelper.writeFlash();
// 獲取Flash對象
FlashHelper.getFlash = function () {
return document.getElementById("flashDomId");
}
// 回調函數
function handleCallback() {
var response = FlashHelper.getFlash().GetVariable("retText");
alert(response);
}
// 發送請求
function request() {
var url = "http://b.qq.com/index.php";
var method = "GET";
var body = "";
var contentType = "application/x-www-form-urlencoded";
var fs = FlashHelper.getFlash();
fs.XmlHttp(url, "handleCallback", method, body, contentType);
}
request();
Flash 跨域優點:
-
在不改動後端的情況下,可以兼容 IE7 及以下瀏覽器的 ajax 跨域問題。
-
crossdoamin.xml 文件可以進行授權訪問控制。
Flash 跨域缺點:
-
受限於瀏覽器對於 Flash 插件的支持程度。
-
沒有調用錯誤的處理。
-
服務器代理
服務器代理,顧名思義即在發送跨域請求時,後端進行代理中轉請求至服務器端,然後將獲取的數據返回給前端。一般適用於以下場景:
針對 IE7 及以下瀏覽器摒棄 Flash 插件的情況,配置代理接口與前端頁面同源,並中轉目標服務器接口,則 ajax 請求不存在跨域問題。
外網前端頁面無法訪問內網接口,配置代理接口允許前端頁面訪問,並中轉內網接口,則外網前端頁面可以跨域訪問內網接口。
服務器代理實現流程:
服務器代理優點:
- 在不使用 Flash 的情況下,兼容不支持 CORS 的瀏覽器跨域請求。
服務器代理缺點:
- 後端需要一定的改造工作量。
(二)前端跨域通信解決方案
前端跨域通信是指瀏覽器中兩個不符合同源策略的前端頁面進行通信。那麼,這種跨域問題,如何進行解決呢。本文總結以下四種常見解決方案:
-
document.domain+iframe
此方案僅適用於主域相同,子域不同的前端通信跨域場景。如下圖所示,兩個不符合同源策略的頁面 http://a.qq.com/a.html 和 http://b.qq.com/b.html,其主域相同爲 qq.com。a.html 嵌套 b.html,再都通過 js 設置 document.domain 爲主域 qq.com,則兩個頁面滿足了同源策略,從而實現了跨域通信。
document.domain+iframe 方案代碼示例:
<!-- A頁面 http://a.qq.com/a.html -->
<iframe src="http://b.qq.com/b.html"></iframe>
<script>
document.domain = "qq.com";
var windowB = document.getElementById("iframe").contentWindow;
alert("B頁面的user變量:" + windowB.user);
</script>
<!-- B頁面 http://b.qq.com/b.html -->
<script>
document.domain = "qq.com";
var user = "saramliu";
</script>
document.domain+iframe 方案優點:
- 實現邏輯簡單,無需額外中轉頁面
document.domain+iframe 方案缺點:
-
僅適用於主域相同,子域不同的前端通信跨域場景
-
location.hash+iframe
當兩個不符合同源策略且主域不同的頁面需要進行跨域通信時,可以利用 url 的 hash 值改變但不刷新頁面的特性,實現簡單的前端跨域通信。
通常情況下 http://a.qq.com/a.html 內嵌不同域的 http://b.qq1.com/b.html 時,受瀏覽器安全機制限制,a.html 可以修改 b.html 的 hash 值,但 b.html 不被允許修改不同域的父窗體 a.html 的 hash 值。因此,此方案需要一個與 a.html 同源的 http://a.qq.com/c.html 來進行中轉,此方案實現流程如下圖所示:
location.hash+iframe 方案代碼示例:
<!-- A頁面 http://a.qq.com/a.html -->
<iframe src="http://b.qq1.com/b.html"></iframe>
<script>
// 監聽c.html傳來的hash值
window.onhashchange = function () {
alert("B頁面傳遞數據:" + location.hash.substring(1));
};
</script>
<!-- B頁面 http://b.qq1.com/b.html -->
<iframe src="http://a.qq.com/c.html"></iframe>
<script>
// 向c.html傳遞hash值
var iframe = document.getElementById('iframe');
setTimeout(function() {
iframe.src = iframe.src + '#user=saramliu';
}, 1000);
</script>
<!-- C頁面 http://a.qq.com/c.html -->
<script>
// 監聽b.html傳來的hash值
window.onhashchange = function () {
// 操作同域a.html的hash值,傳遞數據
window.parent.parent.location.hash = window.location.hash.substring(1);
};
</script>
location.hash+iframe 方案優點:
-
可以解決主域不同的前端通信跨域問題。
-
hash 改變,頁面不會刷新。
location.hash+iframe 方案缺點:
-
受部分瀏覽器安全機制限制,需要額外的同源中轉頁面,且中轉頁面需要 js 邏輯來修改 hash 值。
-
通信數據類型及長度均受限,且數據外顯在 url 上,存在一定安全風險。
-
window.name+iframe
window.name 屬性的獨特之處在於,name 值在不同頁面(甚至不同域名)加載後依舊存在,並且可以支持非常長的 name 值(2MB)。
如下圖所示,http://a.qq.com/a.html 內嵌不同域的 http://b.qq1.com/b.html。b.html 有數據要傳遞時,把數據附加到 window.name 上,然後跳轉到一個和 a.html 同域的 http://a.qq.com/c.html。由於 a.html 和 c.html 滿足同源策略,a.html 可以獲取 c.html 的 window.name,從而實現了跨域通信。
window.name+iframe 代碼示例:
<!-- A頁面 http://a.qq.com/a.html -->
<iframe src="http://b.qq1.com/b.html"></iframe>
<script>
var state = 0;
var iframe = document.getElementById('iframe');
iframe.onload = function() {
if (state === 1) {
// 第2次onload成功後,讀取同域window.name中數據
alert(iframe.contentWindow.name);
} else if (state === 0) {
// 第1次onload成功後
state = 1;
}
};
</script>
<!-- B頁面 http://b.qq1.com/b.html -->
<script>
window.name = "這裏是B頁面!";
window.location = "http://a.qq.com/c.html";
</script>
window.name+iframe 方案優點:
-
可以解決主域不同的前端通信跨域問題。
-
通信數據類型不受限,且長度可達 2MB。
window.name+iframe 方案缺點:
-
需要額外的同源中轉頁面,但中轉頁可以爲空白頁。
-
postMessage
postMessage 是 HTML5 XMLHttpRequest Level2 中的 API,且是爲數不多可以跨域操作的 window 屬性之一,它通常用於解決以下方面的問題:
-
頁面和其打開的新窗口的數據傳遞。
-
多窗口之間消息傳遞。
-
頁面與嵌套 iframe 消息傳遞。
postMessage 是一種安全的跨域通信方法。當 a.html 獲得對 b.html 的 window 對象後,a.html 調用 postMessage 方法分發一個 MessageEvent 消息。b.html 通過監聽 message 事件即可獲取 a.html 傳遞的數據,從而實現跨域通信。postMessage 實現流程如下圖所示:
postMessage 方法的語法如下:
otherWindow.postMessage(message、targetOrigin、[transfer])
- otherWindow
目標窗口的 window 對象,比如 iframe 的 contentWindow 屬性、執行 window.open 返回的 window 對象等。
- message
將要發送給其他 window 的數據。
- targetOrigin
指定哪些窗口能接收到消息事件,其值可以是字符串 *(表示無限制)或者是 “協議 + 主機 + 端口號”。
- transfer(可選)
是一串和 message 同時傳遞的 Transferable 對象,這些對象的所有權將被轉移給消息的接收方,而發送一方將不再保有所有權。
postMessage 方案代碼示例:
<!-- A頁面 http://a.qq.com/a.html -->
<iframe src="http://b.qq1.com/b.html"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {meesage: "這裏是A頁面發的消息"};
var url = "http://b.qq1.com/b.html";
// 向B頁面發送消息
iframe.contentWindow.postMessage(JSON.stringify(data), url);
};
window.addEventListener("message", function(e) {
alert("B頁面發來消息:" + JSON.parse(e.data));
});
</script>
<!-- B頁面 http://b.qq1.com/b.html -->
<script>
window.addEventListener("message", function(e) {
alert("A頁面發來消息:" + JSON.parse(e.data));
var data = {meesage: "這裏是B頁面發的消息"};
var url = "http://a.qq.com/a.html";
window.parent.postMessage(JSON.stringify(data), url);
}, false);
</script>
postMessage 方案優點:
- 可以解決多種類型的前端跨域通信問題;
postMessage 方案缺點:
- 兼容性方面相對差一點,IE8 及以下瀏覽器不支持該方法,IE9 只支持 postMessage 傳遞 string 類型的數據,而標準的 postMessage 消息數據可以是任何類型。
四、總結
本文介紹了瀏覽器受同源策略限制的跨域行爲以及常見的跨域場景。總結了跨域問題的經驗,並從 ajax 請求和前端通信兩大方向進行梳理常用的跨域解決方法及其優缺點,希望可以作爲大家在日常開發解決 web 跨域問題的參考。如果有描述不當之處,也希望大家隨時進行溝通和指正。
參考資料:
-
瀏覽器的同源策略
-
跨源資源共享(CORS)
3.Cross-domain AJAX using Flash
4.window.postMessage
** 作者簡介**
劉孟
騰訊前端開發工程師
騰訊前端開發工程師,畢業於上海大學。目前負責騰訊優聯項目的前端開發工作,有豐富的系統平臺及遊戲營銷活動前端開發經驗。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ggOxPTouwvWBgh5gc5CIvw