前端跨域問題
同源政策是 瀏覽器
出於安全考慮,提出的一種重要安全機制,通過限制了不同源之間的互動,以隔離潛在的惡意檔案對網站帶來的安全問題。
那什麼是同源,也就是跨域是什麼?
一個源的定義:如果兩個頁面協議,埠和域名都相同,則兩個頁面屬於同一源
下表列舉了同源比較的示例:
URL | Outcome | Reason |
---|---|---|
ofollow,noindex">http://store.company.com/dir2/other.html | success | |
http://store.company.com/dir/inner/another.html | success | |
https://store.company.com/secure.html | failure | 不同協議 |
http://store.company.com:81/dir/etc.html | failure | 不同埠 |
http://news.company.com/dir/other.html | failure | 不同主機 |
同源政策限制了不同源的互動,其中這裡的互動主要分為三類:
- 允許跨域寫操作:像連結,重定向自己表單提交
- 允許跨域資源嵌入:像
<script>,<link rel="stylesheet">,<img>,<video>,<iframe>
等 - 不允許跨域讀操作,常見的有
注意:cookies使用不同的源方式定義:一個頁面允許為本域和任何父域設定cookie,只要父域不是公共字尾。cookie不區分協議或埠,不管使用哪個協議或埠號,瀏覽器都允許給定的域以及其任何子域名放問cookie。
跨域解決方案
頁面之間的跨域通訊
一、document.main + iframe
頁面可以通過dcoument.main來更改自己的源,但是有限制,即只能通過document.domain設定為當前域或當前域的超級域,可以利用這個特性,解決主域相同,不同子域框架間的互動問題
原理:將父視窗和子視窗都設定document.domain為基礎主域,就實現了同域
實現
// www.domain.com/a.html document.domain = 'domain.com'; var ifr = document.createElement('iframe'); ifr.src ='http://www.domain.com/b.html'; ifr.display = none; document.body.appendChild(ifr); ifr.onload = function(){ var doc = ifr.contentDocument || ifr.contentWindow.document; ifr.onload = null; }; // www.domain.com/b.html document.domain = 'domain.com';
此方案只適用於主域相同,子域不同的跨域應用場景中
二、location.hash + iframe
假設 www.aaa.com/a.html
要和 www.bbb.com/b.html
傳遞資訊
www.bbb.com/b.html
兩個不同域的頁面不允許修改parent.location.hash的值,所以要藉助iframe
- 由於location.hash直接暴露在url上,並且在瀏覽器裡產生歷史記錄,資料安全性不高且使用者體驗差
- 由於url大小的限制,支援傳遞的資料量不大
- 有些瀏覽器不支援onhashchange事件,需要輪詢來獲知url的變化
三、Window.name + iframe
window物件的name屬性,該屬性有以下特徵:
一個window.name 持久存在
比如我們在任意一個頁面執行下面程式碼
window.name = "My window's name"; setTimeout(function(){ window.location.href = "http://example.cn/"; },1000)
進入example.cn頁面後我們列印window.name,就能看到在上個頁面設的值了。可見,在同一個標籤裡跳轉網頁,window.name是不會改變的
原理:A頁a.html獲取B頁面b.html的window.name值,由於跨域,利用iframe,在第一次載入B頁面的時候,切換到一個與A頁面同域的代理中間空白頁proxy.html,在第二次載入proxy頁就能讀取B頁面設定的window.name的值了
實現
var proxy = function(url, callback) { var state = 0; var iframe = document.createElement('iframe'); // 載入跨域頁面 iframe.src = url; // onload事件會觸發2次,第1次載入跨域頁,並留存資料於window.name iframe.onload = function() { if (state === 1) { // 第2次onload(同域proxy頁)成功後,讀取同域window.name中資料 callback(iframe.contentWindow.name); destoryFrame(); } else if (state === 0) { // 第1次onload(跨域頁)成功後,切換到同域代理頁面 iframe.contentWindow.location = 'http://www.aaa.com/proxy.html'; state = 1; } }; document.body.appendChild(iframe); // 獲取資料以後銷燬這個iframe,釋放記憶體;這也保證了安全(不被其他域frame js訪問) function destoryFrame() { iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } }; // 請求跨域b頁面資料 proxy('http://www.bbb.com/b.html', function(data){ alert(data); });
該方法與document.domain相比,放寬了域名字尾要相同的限制,可以從任意頁面獲取string型別的資料
四、Window.postMessage
Window.postMessage
是HTML5引入的一個新API:跨文件通訊,用來解決跨視窗通訊問題。
使用方法:
Window.postMessage(message, targetOrigin, [transfer])
-
message
是傳送到其他window的資料,其值可以是物件也可以是字串(IE8,9) -
targetOrigin
用來指定哪些視窗能接收到訊息事件,其值可以是字串"*"
(不推薦),表示不限制域名,向所有視窗傳送;也可以是一個URI,即”協議 + 域名 + 埠”,只有當目標視窗協議、域名和埠三者完全匹配,訊息才能被髮送。
兩個視窗之間通過註冊message事件來監聽訊息,message事件的的事件物件event,提供三個屬性:
- event.data: 傳送的資料
- event.origin: 訊息傳送方的origin,由”協議 + 域名 + 埠”拼接而成
- event.source:傳送訊息的視窗物件的引用
相容性
注意到IE8+, chrome,Firefox等主流瀏覽器都支援這個功能。但是在IE8和9以及Firefox 6.0和更低版本僅支援字串作為postMessage的訊息。
實現
假設A頁面域名是 http://www.aaa.com
,B視窗域名是 http://www.bbb.com
,B頁面向A視窗傳送訊息
/* * B頁面, 傳送訊息<http://www.bbb.com> */ window.onload = function() { window.parent.postMessage(JSON.stringify({ method: method1, args: ['args'], }), 'http://aaa.com'); };
/* * A頁面, 接收訊息<http://www.aaa.com> */ var supportMethodList = { method1: function () { //... } ... }; window.addEventListener('message', function (e) { // 確保訊息為信任來源 if (e.origin !== http://aaa.com)) { return; } var message = { method: null, args: [] }; try { // 僅使用 JSON 字串,以支援 IE8/9 message = JSON.parse(e.data); } catch (e) {} if (!supportMethodList[message.method]) { return; } supportMethodList[message.method].apply(context,message.args); }, false);
如果不考慮低版本IE,此方法,是目前解決iframe之間互動的比較好的方案
AJAX請求不同源的跨域
一、JSONP
同源策略允許跨域資源嵌入,利用這個特性,通過建立一個 <script>
元素,向伺服器請求JSON資料,這種做法不受同源策略限制,伺服器收到請求後,將資料放在一個指定的名字的回撥函式裡傳回來,從而實現跨域通訊
// 客戶端 <script> var script = document.createElement('script'); script.type = 'text/javascript'; // 傳參並指定回撥執行函式為onBack script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack'; document.head.appendChild(script); // 回撥執行函式 function onBack(res) { alert(JSON.stringify(res)); } </script> // 服務端 返回 onBack({"status": true, "user": "admin"})
此方法雖然簡單,但只支援GET請求
二、Socket/">WebSocket
WebSocket是一種通訊協議,使用ws://(非加密)和wss://(加密)作為協議字首。該協議不受同源政策限制,只要伺服器支援,就可以通過它進行跨源通訊。
三、CORS
CORS是一個W3C標準,全稱是”跨域資源共享”(Cross-origin resource sharing)
跨域資源共享( CORS )機制允許 Web 應用伺服器進行跨域訪問控制,從而使跨域資料傳輸得以安全進行。瀏覽器支援在 API 容器中(例如 XMLHttpRequest 或 Fetch )使用 CORS,以降低跨域 HTTP 請求所帶來的風險。
CORS需要瀏覽器和伺服器同時支援。目前,所有瀏覽器都支援該功能,IE瀏覽器不能低於IE10。
瀏覽器將CORS請求分成:簡單請求、預檢請求和附帶憑證資訊的請求
簡單請求

滿足下面兩個條件的瀏覽器就視為簡單請求:
- 只使用 GET, HEAD 或者 POST 請求方法。如果使用 POST 向伺服器端傳送資料,則資料型別(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一種。
- 不會使用自定義請求頭(類似於 X-Modified 這種)。
比如,假如站點 http://foo.example
的網頁應用想要訪問 http://bar.other
的資源。 http://foo.example
的網頁中可能包含類似於下面的 JavaScript 程式碼:
var invocation = new XMLHttpRequest(); // 注意 此處url是絕對路徑 var url = 'http://bar.other/resources/public-data/'; function callOtherDomain() { if(invocation) { invocation.open('GET', url, true); invocation.onreadystatechange = handler; invocation.send(); } }
如上,通過使用 Origin 和 Access-Control-Allow-Origin 就可以完成最簡單的跨站請求。不過伺服器需要把 Access-Control-Allow-Origin 設定為 * 或者包含由 Origin 指明的站點(協議 + 域名 + 埠)。
頭資訊的Origin欄位是瀏覽器自動新增的
預檢請求

當滿足以下條件的則為預檢請求
- 請求以 GET, HEAD 或者 POST 以外的方法發起請求。或者,使用 POST,但請求資料為 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的資料型別。比如說,用 POST 傳送資料型別為 application/xml 或者 text/xml 的 XML 資料的請求。
- 使用自定義請求頭(比如新增諸如 X-PINGOTHER)
例如:
var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/post-here/'; var body = '{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}Arun'; function callOtherDomain(){ if(invocation){ invocation.open('POST', url, true); invocation.setRequestHeader('X-PINGOTHER', 'pingpong'); invocation.setRequestHeader('Content-Type', 'application/xml'); invocation.onreadystatechange = handler; invocation.send(body); } }
客戶端傳送請求頭主要資訊:
Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER
伺服器成功響應返回部分資訊:
Access-Control-Allow-Origin: http://foo.example //表明伺服器允許http://foo.example的請求 Access-Control-Allow-Methods: POST, GET, OPTIONS //表明伺服器可以接受POST, GET和 OPTIONS的請求方法 Access-Control-Allow-Headers: X-PINGOTHER //傳遞一個可接受的自定義請求頭列表。伺服器也需要設定一個與瀏覽器對應。否則會報 Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers in preflight response 的錯誤 Access-Control-Max-Age: 1728000 //告訴瀏覽器,本次“預請求”的響應結果有效時間是多久。在上面的例子裡,1728000秒代表著20天內,瀏覽器在處理針對該伺服器的跨站請求,都可以無需再發送“預請求”,只需根據本次結果進行判斷處理。
附帶憑證資訊的請求
CORS請求預設不傳送Cookie和HTTP認證資訊。如果要把Cookie發到伺服器,一方面要伺服器同意,指定Access-Control-Allow-Credentials欄位。另一方面,開發者必須在AJAX請求中開啟withCredentials屬性。
程式碼如下:
// http://foo.example站點的指令碼向http://bar.other站點發送一個GET請求,並設定了一個Cookies值。指令碼程式碼如下: var invocation = new XMLHttpRequest(); var url = 'http://bar.other/resources/credentialed-content/'; function callOtherDomain(){ if(invocation) { invocation.open('GET', url, true); invocation.withCredentials = true; invocation.onreadystatechange = handler; invocation.send(); } }
假設服務端返回成功響應部分訊息如下:
Access-Control-Allow-Origin: http://foo.example Access-Control-Allow-Credentials: true Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
需要注意的是,對於附帶身份憑證的請求,伺服器不能設定 Access-Control-Allow-Origin 的值為 “*”
,必須指定明確的、與請求網頁一致的域名。在上面例子中,
Access-Control-Allow-Origin 的值應該設定為 http://foo.example
,請求才能被成功執行
CORS支援所有型別的HTTP請求,是跨域HTTP請求的根本解決方案。
誤區
-
動態請求就會有跨域問題
跨域是瀏覽器行為,不存在與java/node/python等環境。
-
跨域就是請求發不出去
跨域請求能發出去,服務端能收到請求並正常返回結果,只是結果被瀏覽器攔截了,最好的例子就是CSRF跨站攻擊原理,但是有一個特例,有些瀏覽器不允許從 HTTPS 的域跨域訪問 HTTP,比如Chrome 和 Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求
在平時的專案開發中,我們並沒有處理與後端介面請求的跨域,卻沒出息跨域問題,是因為我們用node搭建了http服務,通過node來轉發uri,node服務和後端服務之間的不存在跨域的。
再次重申,跨域是瀏覽器的行為。