八種方式實現跨域請求
瀏覽器的同源策略
? 提到跨域不能不先說一下”同源策略”。
? 何為同源?只有當協議、端口、和域名都相同的頁面,則兩個頁面具有相同的源。只要網站的 協議名protocol、 主機host、 端口號port 這三個中的任意一個不同,網站間的數據請求與傳輸便構成了跨域調用,會受到同源策略的限制。
? 同源策略限制從一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的關鍵的安全機制。瀏覽器的同源策略,出於防範跨站腳本的攻擊,禁止客戶端腳本(如 JavaScript)對不同域的服務進行跨站調用(通常指使用XMLHttpRequest請求)。
方式一:圖片ping或script標簽跨域
圖片ping常用於跟蹤用戶點擊頁面或動態廣告曝光次數。
script標簽可以得到從其他來源數據,這也是JSONP依賴的根據。
缺點:只能發送Get請求 ,無法訪問服務器的響應文本(單向請求)
方式二:JSONP跨域
? JSONP(JSON with Padding)是數據格式JSON的一種“使用模式”,可以讓網頁從別的網域要數據。根據 XmlHttpRequest 對象受到同源策略的影響,而利用 <script>
元素的這個開放策略,網頁可以得到從其他來源動態產生的JSON數據,而這種使用模式就是所謂的 JSONP。用JSONP抓到的數據並不是JSON,而是任意的JavaScript,用 JavaScript解釋器運行而不是用JSON解析器解析。所有,通過Chrome查看所有JSONP發送的Get請求都是js類型,而非XHR。
缺點:
- 只能使用Get請求
- 不能註冊success、error等事件監聽函數,不能很容易的確定JSONP請求是否失敗
- JSONP是從其他域中加載代碼執行,容易受到跨站請求偽造的攻擊,其安全性無法確保
方式三:CORS
? Cross-Origin Resource Sharing(CORS)跨域資源共享是一份瀏覽器技術的規範,提供了 Web 服務從不同域傳來沙盒腳本的方法,以避開瀏覽器的同源策略,確保安全的跨域數據傳輸。現代瀏覽器使用CORS在API容器如XMLHttpRequest來減少HTTP請求的風險來源。與 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。服務器一般需要增加如下響應頭的一種或幾種:
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400
跨域請求默認不會攜帶Cookie信息,如果需要攜帶,請配置下述參數:
"Access-Control-Allow-Credentials": true // Ajax設置 "withCredentials": true
方式四:window.name+iframe
? window.name
通過在iframe(一般動態創建i)中加載跨域HTML文件來起作用。然後,HTML文件將傳遞給請求者的字符串內容賦值給window.name
。然後,請求者可以檢索window.name值作為響應。
- iframe標簽的跨域能力;
- window.name屬性值在文檔刷新後依舊存在的能力(且最大允許2M左右)。
每個iframe都有包裹它的window,而這個window是top window的子窗口。contentWindow屬性返回<iframe>
元素的Window對象。你可以使用這個Window對象來訪問iframe的文檔及其內部DOM。
<!-- 下述用端口 10000表示:domainA 10001表示:domainB --> <!-- localhost:10000 --> <script> var iframe = document.createElement(‘iframe‘); iframe.style.display = ‘none‘; // 隱藏 var state = 0; // 防止頁面無限刷新 iframe.onload = function() { if(state === 1) { console.log(JSON.parse(iframe.contentWindow.name)); // 清除創建的iframe iframe.contentWindow.document.write(‘‘); iframe.contentWindow.close(); document.body.removeChild(iframe); } else if(state === 0) { state = 1; // 加載完成,指向當前域,防止錯誤(proxy.html為空白頁面) // Blocked a frame with origin "http://localhost:10000" from accessing a cross-origin frame. iframe.contentWindow.location = ‘http://localhost:10000/proxy.html‘; } }; iframe.src = ‘http://localhost:10001‘; document.body.appendChild(iframe); </script> <!-- localhost:10001 --> <!DOCTYPE html> ... <script> window.name = JSON.stringify({a: 1, b: 2}); </script> </html>
註意:
- 直接嵌入其他域(localhots:10001)下的URL會報錯,所以需要加載完成替換為當前域的URL(localhots:10000),
proxy.html
為空白頁面,只為解決該問題; - 重新設置src(http://localhost:10000/proxy.html)後導致頁面不斷刷新,所以通過
state
來控制; - 全部獲取完結果後,清除該iframe。
方式五:window.postMessage()
? HTML5新特性,可以用來向其他所有的 window 對象發送消息。需要註意的是我們必須要保證所有的腳本執行完才發送 MessageEvent,如果在函數執行的過程中調用了它,就會讓後面的函數超時無法執行。
下述代碼實現了跨域存儲localStorage
<!-- 下述用端口 10000表示:domainA 10001表示:domainB --> <!-- localhost:10000 --> <iframe src="http://localhost:10001/msg.html" name="myPostMessage" style="display:none;"> </iframe> <script> function main() { LSsetItem(‘test‘, ‘Test: ‘ + new Date()); LSgetItem(‘test‘, function(value) { console.log(‘value: ‘ + value); }); LSremoveItem(‘test‘); } var callbacks = {}; window.addEventListener(‘message‘, function(event) { if (event.source === frames[‘myPostMessage‘]) { console.log(event) var data = /^#localStorage#(\d+)(null)?#([\S\s]*)/.exec(event.data); if (data) { if (callbacks[data[1]]) { callbacks[data[1]](data[2] === ‘null‘ ? null : data[3]); } delete callbacks[data[1]]; } } }, false); var domain = ‘*‘; // 增加 function LSsetItem(key, value) { var obj = { setItem: key, value: value }; frames[‘myPostMessage‘].postMessage(JSON.stringify(obj), domain); } // 獲取 function LSgetItem(key, callback) { var identifier = new Date().getTime(); var obj = { identifier: identifier, getItem: key }; callbacks[identifier] = callback; frames[‘myPostMessage‘].postMessage(JSON.stringify(obj), domain); } // 刪除 function LSremoveItem(key) { var obj = { removeItem: key }; frames[‘myPostMessage‘].postMessage(JSON.stringify(obj), domain); } </script> <!-- localhost:10001 --> <script> window.addEventListener(‘message‘, function(event) { console.log(‘Receiver debugging‘, event); if (event.origin == ‘http://localhost:10000‘) { var data = JSON.parse(event.data); if (‘setItem‘ in data) { localStorage.setItem(data.setItem, data.value); } else if (‘getItem‘ in data) { var gotItem = localStorage.getItem(data.getItem); event.source.postMessage( ‘#localStorage#‘ + data.identifier + (gotItem === null ? ‘null#‘ : ‘#‘ + gotItem), event.origin ); } else if (‘removeItem‘ in data) { localStorage.removeItem(data.removeItem); } } }, false); </script>
註意Safari下會報錯:
? Blocked a frame with origin “http://localhost:10001” from accessing a frame with origin “http://localhost:10000“. Protocols, domains, and ports must match.
避免該錯誤,可以在Safari瀏覽器中勾選開發菜單==>停用跨域限制。或者只能使用服務器端轉存的方式實現,因為Safari瀏覽器默認只支持CORS跨域請求。
方式六:修改document.domain跨子域
? 前提條件:這兩個域名必須屬於同一個基礎域名!而且所用的協議,端口都要一致,否則無法利用document.domain
進行跨域,所以只能跨子域
? 在根域範圍內,允許把domain屬性的值設置為它的上一級域。例如,在”aaa.xxx.com”域內,可以把domain設置為 “xxx.com” 但不能設置為 “xxx.org” 或者”com”。
現在存在兩個域名aaa.xxx.com和bbb.xxx.com。在aaa下嵌入bbb的頁面,由於其document.name不一致,無法在aaa下操作bbb的js。可以在aaa和bbb下通過js將document.name = ‘xxx.com‘;設置一致,來達到互相訪問的作用。
方式七:WebSocket
WebSocket protocol 是HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通信,同時允許跨域通訊,是server push技術的一種很棒的實現。相關文章,請查看:WebSocket、WebSocket-SockJS
**需要註意:**WebSocket對象不支持DOM 2級事件偵聽器,必須使用DOM 0級語法分別定義各個事件。
方式八:代理
同源策略是針對瀏覽器端進行的限制,可以通過服務器端來解決該問題
DomainA客戶端(瀏覽器) ==> DomainA服務器 ==> DomainB服務器 ==> DomainA客戶端(瀏覽器)
實現HTTP、HTTPS代理請參照: 創建HTTP與HTTPS服務器與客戶端
八種方式實現跨域請求