在開始之前,我們先熟悉這樣一個概念:同源策略。所謂同源策略,指的是‘同一個協議,同一個域名,同一個端口’。三者有任意一個不一樣,均不可稱之為同源。
URL | 說明 | 是否允許請求 |
http://www.a.com/a.js http://www.a.com/b.js |
同一協議,同一域名,同一端口 | 是 |
http://www.a.com/a.js https://www.a.com/b.js |
不同協議,同一域名,同一端口 | 否 |
http://www.a.com/a.js https://www.b.com/b.js |
不同協議,不同域名,同一端口 | |
http://www.a.com/a.js https://www.a.com:8000/b.js |
不同協議,不同域名,不同端口 | |
http://www.a.com/a.js http://www.b.com/b.js |
同一協議,不同域名,同一端口 | |
http://www.a.com/a.js http://www.a.com:8000/b.js |
同一協議,同一域名,不同端口 | |
http://www.a.com/a.js http://192.168.98.123/b.js |
域名與域名對應ip | |
http://www.a.com/a.js http://nba.a.com/b.js |
主域相同,子域不同 | |
http://www.a.com/a.js http://a.com/b.js |
同一域名,不同二級域名 |
從上邊可以看出,引起跨域問題的場景很多。我們接下來就簡單的來說一下跨域問題的存在及其解決方案。
避開跨域
比如在a.com域名下的某個頁面需要跨域請求b.com下的資源文件。這時候我們可以把執行跨域的js文件放到b.com下。而在a.com中通過外聯的方式引入b.com下的文件,這樣js文件與圖片等資源都在同一域名下,即可正常訪問。使用場景比如:cdn,圖像……
document.domain + iframe
對於主域相同,子域不同引起的跨域問題,我們可以通過document.domain的方式來解決。
具體可以在http://www.a.com/a.html和http://script.a.com/b.html中分別設置:document.domain = 'a.com',然后在a.html中創建一個iframe,這樣就可以實現通信了。
http://www.a.com/a.html 部分設置如下:
document.domain = 'a.com'; function addIframe () { var iframe = document.createElement('iframe'); iframe.src = 'http://script.a.com/b.html'; iframe.style.display = 'none'; docuemnt.body.append(iframe); iframe.onload = function () { var ele = iframe.contentDocument || iframe.contentWindow.docuemnt; console.log(ele.getElementsByTagName('h1')[0].childNodes[0].nodeValue); } }
http://script.a.com/b.html 部分設置如下:
document.domain = 'a.com';
那么問題來了:
- 安全性:當一個站點收到攻擊后,另外一個站點也會引起安全泄露
- 耦合性:當頁面中有多個iframe時,要想能操作所有iframe,則必須設置相同的document.domain
location.hash + iframe
由於地址欄上的hash值發生改變,並不會觸發頁面刷新,所以可以利用這個原理來跨域請求數居。但是數據大小有限制。
假設a.com域名下的文件a.html需要跨域請求b.com下的文件b.html。采用location.hash來處理如下:在a.html中創建一個隱藏的iframe,設置src指向b.com下的b.html,把hash值當做參數傳遞過去。b.html在接收到請求后,通過修改a.html上的hash值來傳遞數據。然后在a.html上設置一個定時監測其地址欄hash值變化的定時器即可。
a.html部分代碼如下:
function addFrame () { var ifr = document.createElement("iframe"); ifr.src = 'http://www.b.com/b.html#proto'; ifr.style.display = "none"; document.body.append(ifr); } function checkHash() { window.addEventListener("hashchange",function(e){ var hash = window.location.hash ? window.location.hash.subString(1) :""; console.log("你請求到的信息是:"+ hash); }); } setInterval(checkHah,1000);
b.html 部分代碼如下:
if(location.hash === '#proto'){ var data = "要返回給a.html的數據"; try{ parent.location.hash = data ; }catch(e){ // ie、chrome的安全機制無法修改parent.location.hash, // 所以要利用一個中間的cnblogs域下的代理iframe var ifr = document.createElement("iframe"); ifr.src="http://www.a.com/test.html#"+data; ifr.style.display = "none"; document.body.append(ifr); } }
test.html 部分代碼如下:
parent.parent.location.href = location.hash.substring(1);
這樣做也會產生一些問題:
- 數據直接暴露在地址欄
- 數據傳輸量有限
- 對瀏覽器歷史記錄產生影響
- 地址欄上傳輸漢字問題
- 步驟繁瑣
postMessage
postMessag是html5新增的API。用於支持web的實時消息傳遞。
使用方法:otherWindow.postMessage(message, targetOrigin);
- otherWindow:對發送信息頁面的引用。可以是頁面中iframe的contentWindow屬性;window.open的返回值;通過name或下標從window.frames取到的值。
- message:要發送的消息
- targetOrigin:接受消息的窗口,設置為*則不做限制。
a.com/a.html 部分代碼如下:
<iframe id="ifr" src="http://www.b.com/b.html"></iframe>
<script>
var ifr = document.getElementById("ifr"),
url = "http://www.b.com";
ifr.contentWindow.postMessage("hello world !",url); </script>
www.b.com/b.html 部分代碼如下:
window.addEventListener("message",function(e){ if(e.origin === 'http://a.com'){ console.log(e.data) } },false)
動態創建script
盡管瀏覽器有同源策略的限制。但是通過script標簽的src屬性卻可以訪問其它域名下的文件,並可以執行其中的函數。下邊先看一下判斷js加載是否完畢的方法:
js.onload = js.onreadystatechange = function() { if(!this.readyState || this.readyState === "loaded" || this.readyState ==="complete"){ js.onload = js.onreadystatechange = null; } }
JSONP
創建一個回調函數,在遠程服務器上執行這個函數,並把json數據當做參數傳遞。
JSONP原理
function addScript (src) { var spt = document.createElement("script"); spt.setAttribute("type","text/javascript"); spt.src= src; document.body.append(spt); } function handleFn(data){ //處理跨域回調數據 } window.onload = function() { addScript("http://localhost:2000/MyService.ashx?callback=handleFn") }
$.getJSON
先看一下,getJSON方法在跨域問題中的使用方法:
$.getJSON("http://localhost:20002/MyService.ashx?callback=?",function(data) { //處理回調函數 })
像上面那樣,我們看到了一個:callback=?。這樣getJSON方法才會知道是用JSONP方式去訪問服務,callback后面的那個問號是內部自動生成的一個回調函數名。那么問題是:如果服務器上規定了回調函數名,我們應該怎么處理呢?答案是:ajax。
ajax
$.ajax({ url:"http://localhost:20002/MyService.ashx?callback=?", dataType:"jsonp", jsonpCallback:"person", success:function(data) { //處理回調函數 } })
jsonpCallback就是可以指定我們自己的回調方法名person,遠程服務接受callback參數的值就不再是自動生成的回調名,而是person。dataType是指定按照JSOPN方式訪問遠程服務。
接下來才是我們這篇文章的重點。
跨域資源共享(CORS)
用於授權資源的跨域訪問 。比如在前端開發中,A站點需要訪問B站點下的某一個接口,從而得到我們想要的數據,這個時候涉及到一個重要的概念:Access-Control-Allow-Origin。如果服務器端在返回頭中沒有設置這個屬性,或者設置的屬性值不包括我們當前的域名A,那么這個時候,當前域名A是不被服務器允許進行跨域資源訪問的。
上邊這幅圖描述了一個完整的CORS請求過程。
CORS請求的過程如下:
- 首先判斷是不是簡單請求,如果是,如:get請求,只需要在HTTP Response后添加Access-Control-Allow-Origin;
- 如果不是簡單請求,如POST,PUT,DELETE等,這個時候瀏覽器會分2次進行請求:一次是預檢請求preflight(method: OPTIONS),主要用於檢測請求來源是否合法,並返回Header。第二次才是真正的請求,所以服務器必須處理OPTIONS應答。
服務器處理請求解析如下:
- 首先檢測Http頭部中是否有origin字段信息
- 如果沒有或者不被允許,則當做普通請求處理,結束;
- 如果有並且是在允許范圍內,則判斷是否是復雜請求;
- 如果是復雜請求,則先執行預檢請求preflight(method: OPTIONS),返回Allow-Headers、Allow-Methods等,內容為空;進如步驟6;
- 如果是簡單請求,則直接進步步驟6;
- 執行請求,返回Allow-Origin、Allow-Credentials等,並返回正常內容。
在HTML5中,也有一些元素為CORS提供了支持,如img,video。此時需要設置crossOrigin屬性,屬性值可以是anonymous
或use-credentials。比如我們要用canvas訪問跨域圖片,就可以像下邊這樣操作:
var img = new Image(), canvas = document.createElement("canvas"), cxt = canvas.getContext("2d"); img.crossOrigin = "Anonymous";//使用CORS img.src="http://img5.imgtn.bdimg.com/it/u=104961686,3757525983&fm=27&gp=0.jpg"; img.onload = function() {
canvas.width = img.width; canvas.height = img.height; cxt.drawImage(img,0,0);
document.body.append(canvas); localStorage.setItem("imgData",canvas.toDataURL("image/jpg")) }