最新博客站點:歡迎來訪
一、同源與同源策略
我們知道,同源指的是協議、域名、端口號全部相同。同源策略(Same Origin Policy)是一種約定,它是瀏覽器最核心也是最基本的安全功能,如果缺少了同源策略,則瀏覽器的正常功能都可能會受到影響。Web是構建在同源策略基礎之上的,瀏覽器只是針對同源策略的一種實現。同源策略是處於對用戶安全的考量的,如果缺少了同源的限制,那又怎么能夠確定別人的網站始終對你是友好的呢。針對非同源的情況制定了一些限制條件,1. 無法讀取不同源的cookie、LocalStorage、indexDB。2. 無法獲得不同源的DOM。3. 不能向不同源的服務器發送Ajax請求。
在瀏覽器中,<script>
、<img>
、<iframe>
、<link>
等標簽都可以跨域加載資源,而不受同源策略的限制。事實上,在大多數情境下,我們經常是需要借用非同源來提供數據的,所以這就要用到跨域方面的技術了。
二、JSONP
JSONP是指JSON Padding,JSONP是一種非官方跨域數據交換協議,由於script的src屬性可以跨域請求,所以JSONP利用的就是瀏覽器的這個原理,需要通信時,動態插入一個javascript標簽。請求的地址一般帶有一個callback參數,假設需要請求的地址為http://localhost:3000?callback=show,服務器返回的代碼一般是show()的JSON數據,而show函數恰恰是前端需要用的這個數據的函數。JSONP非常簡單易用,自動補全API利用的就是JSONP。
一個簡單的例子:
var script = doxument.createElement("script"); script.setAttribute("type", "text/javascript"); script.src="http://example.com/ip?callback=handleResponse"; document.body.appendChild(script); function handleResponse(data) { console.log('Your public IP address is: '+data.ip); }
JSONP解決跨域的本質:<script>標簽可以請求不同域名下的資源,即<script>請求不受瀏覽器同源策略的影響。上例中的script會向http://example.com/服務器發送請求,這個請求的url后面帶了個callback參數,是用來告訴服務器回調方法的方法名的。因為服務器收到請求后,會把相應的數據寫進handleResponse的參數,也就是服務器會返回如下的腳本:
handleResponse({ "ip" : "8.8.8.8" });
這樣瀏覽器通過<script>下載的資源就是上面的腳本了,<script>下載完就會立即執行,也就是說http://example.com/ip?callback=handleResponse
這個請求返回后就會立即執行上面的腳本代碼,而這個腳本代碼就是調用回調方法和拿到json數據了。
我們再來看一個例子:
//請求代碼 function jsonp(callback) { var script = document.createElement("script"); url = `https://localhost:3000?callback=${callback}`; script.setAttribute("src", url); document.querySelector("head").appendChild(script); } function show(data) { concole.log(`學生姓名為: ${data.name},年齡為: ${data.age},性別為: ${data.sex}`); } jsonp("show"); //響應代碼 const student = { name: "Knight", age: 19, sex: "male" }; var callback = url.parse(req.url, true).query.callback; res.writeHead(200,{ "Content-Type": "application/json;charset=utf-8" }); res.end(`${callback}(${JSON.stringify(student)})`);
JSONP有一個很大問題,就是只能進行GET請求。
三、跨域源資源共享(CORS)
CORS是W3C制定的跨站資源分享標准,可以讓AJAX實現跨域訪問,定義了在必須訪問跨域資源時瀏覽器與服務器該如何溝通。CORS背后的基本思想,就是使用自定義的HTTP頭部讓瀏覽器和服務器進行溝通,從而決定請求或響應應該成功還是失敗。
比如一個簡單的使用GET或POST的請求,它沒有自定義的頭部,而主體內容是text/plain。在發送該請求時,需要給它附加一個額外的Origin頭部,其中包含請求頁面的源信息(協議、域名、端口號),以便服務器根據該頭部信息來決定是否給予響應。
Origin: http://www.example.com
如果服務器認為這個請求可以接受,就在Access-Control-Allow-Origin頭部中發回相同的源信息(如果是公共資源,可以發“*”)。例如:
Access-Control-Allow-Origin: http://www.example.com
如果沒有這個頭部信息或信息不匹配,瀏覽器就會駁回請求。正常情況下,瀏覽器會處理請求。此時,請求和響應都不包含Cookie信息。
簡單請求的跨域:
請求方式為GET或則POST;
假若請求是POST的話,Content-Type必須為下列之一:
application/x-www-form-urlencoded
mutipart/form-data
text/plain
不含有自定義頭;
對於簡單的跨域只進行一次http請求:
function ajaxPost(url, obj, header) { return new Promise((resolve, reject) => { var xhr=new XMLHttpRequest(), str = '' ; keys = Object.keys(obj); for(var i=0,len=keys.length;i<len;i++) { str +=`${keys[i]}=${obj[keys[i]]}&`; } str = str.substring(0, str.length - 1); xhr.open('post', url); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); if(header instanceof Object) { for(var k in header) xhr.setRequestHeader(k, header[k]); } xhr.send(str); xhr.onreadystatechange = function() { if(xhr.readyState == 4) { if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { resolve(xhr.responseText); } else { reject(); } } } }); } ajaxPost("https://localhost:3000?page=cors", { name: "Knight", age: 19, sex: "male" }).then((text) => {console.log(text);}, ()=>{console.log("請求失敗");}); //后端處理 var postData = ""; req.on("data", (data) => { postData += data; }); req.on("end", () => { postData = querystring.parse(postData); res.writeHead(200, { "Access-Control-Allow-Origin": "*", "Content-Type": "application/json;charset=utf-8" }); if(postData.name === student.name && Number(postData.age) === student.age && postData.sex === student.sex) { res.end(`yeah! ${postData.name} is a good guy!`); } else { res.end("No! a bad guy!"); } });
對於非簡單請求來說,需要兩次http請求,其中在請求之前有一次預檢請求。
function ajaxPost(url, obj, header) { return new Promise((resolve, reject) => { var xhr=new XMLHttpRequest(), str = '' ; keys = Object.keys(obj); for(var i=0,len=keys.length;i<len;i++) { str +=`${keys[i]}=${obj[keys[i]]}&`; } str = str.substring(0, str.length - 1); xhr.open('post', url); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); if(header instanceof Object) { for(var k in header) xhr.setRequestHeader(k, header[k]); } xhr.send(str); xhr.onreadystatechange = function() { if(xhr.readyState == 4) { if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { resolve(xhr.responseText); } else { reject(); } } } }); } ajaxPost("https://localhost:3000?page=cors", { name: "Knight", age: 19, sex: "male" }, {"X-author": "Knight"}).then((text) => {console.log(text);}, ()=>{console.log("Request Error!");}); //后端處理 var postData = ""; if(req.method == "OPTIONS") { res.writeHead(200, { "Access-Control-Max-Age": 3000, "Access-Control-Allow-Headers": "X-author", "Access-Control-Allow-Origin": "*", "Content-Type": "application/json;charset=utf-8" }); res.end(); return void 0; } req.on("data", (data) => { postData += data; }); req.on("end", () => { postData = querystring.parse(postData); res.writeHead(200, { "Access-Control-Allow-Origin": "*", "Content-Type": "application/json;charset=utf-8" }); if(postData.name === student.name && Number(postData.age) === student.age && postData.sex === student.sex) { res.end(`yeah! ${postData.name} is a good guy!`); } else { res.end("No! a bad guy!"); } });
上面代碼中,兩個響應頭: Access-Control-Allow-Headers,用來指明在實際的請求中,可以使用那些自定義的http請求頭;Access-Control-Max-Age,用來指定此次預請求的結果的有效期,在有效期內則不會發出預請求,類似於緩存。
四、document.domain實現跨域
可以將子域和主域的document.domian設為同一個主域來實現跨域。但前提條件是,這兩個域名必須屬於同一個基礎域名,所用的協議,端口都要一致,否則無法通過document.domain()來進行跨域。
example 1:
如果想要在你的http://www.knightboy.cn/a.html頁面里使用<iframe>調用另一個http://knightboy.cn/b.html頁面。這時候你想在a頁面里面獲取b頁面里的DOM,然后進行操作。然后你會發現你不能獲得b的DOM。document.getElementById("myIFrame").contentWindow.document或window.parent.document.body因為兩個窗口不同源而報錯。
這時候你只需要在a頁面里和b頁面里把document.domian設置成相同的值就可以在兩個頁面里操作Dom了。
example 2:
如果你在http://www.knightboy.cn/a.html頁面里寫入了document.cookie = "test=hello world";你在http://knightboy.cn/b.html頁面是拿不到這個cookie的。
原因在於,Cookie是服務器寫入瀏覽器的一小段信息,只有同源的網頁才能共享。但是,兩個網頁一級域名相同,二級域名不同,瀏覽器允許通過設置document.domain來共享Cookie。另外,服務器也可以在設置Cookie的時候,指定Cookie的所屬域名為一級域名。這樣的話,二級域名和三級域名不用做任何設置便可以讀取這個Cookie。
有一點需要注意的是:document.domain雖然可以讀寫,但只能設置成自身或者是高一級的父域且主域必須相同。所以只能解決一級域名相同二級域名不同的跨域問題。還有就是document.domain只適用於Cookie和iframe窗口,LocalStorage和IndexDB無法通過這種方法跨域。
五、window.name跨域
window對象有一個name屬性,該屬性有個特征:即在一個窗口(window)的生命周期內,窗口載入的所有頁面都是共享一個window.name的,每個頁面對window.name都有讀寫的權限,window.name是持久存在一個窗口載入過的所有頁面中的,並不會因新頁面的載入而進行重置。注意,window.name的值只能是字符串的形式,且這個字符串的大小最大能容許2M左右甚至更大的容量,因瀏覽器而異,但一般是夠用的。
example 1:
現在在一個瀏覽器的一個標簽頁里打開http://www.knightboy.cn/a.html頁面,你通過location.href = http://baidu.com/b.html,在同一個瀏覽器標簽頁里打開了不同域名下的頁面。這時候這兩個頁面你可以使用window.name來傳遞參數。因為window.name指的是瀏覽器窗口的名字,只要瀏覽器窗口相同,那么無論在哪個頁面里訪問都是一樣的。
example 2:
你的http://www.knightboy.cn/a.html頁面里使用<iframe>調用另一個http://baidu.com/b.html頁面。這時候你想在a頁面里獲取b頁面里的DOM,然后進行操作。結果會發現不能獲得b中的DOM。同樣會因為不同源而報錯,和上面提到的不同之處就是兩個頁面的一級域名也不相同。這時候document.domain就解決不了了。
瀏覽器窗口有window.name屬性。這個屬性的最大特點就是,無論是否同源,只要在同一個窗口里,前一個網頁設置了這個屬性,后一個網頁可以讀取它。比如當在b頁面里設定window.name="hello",你再返回到a頁面,在a頁面訪問window.name,可以得到hello。這種方法的優點是,window.name容量很大,可以放置非常長的字符串;缺點是必須監聽子窗口window.name屬性的變化,影響網頁性能。
<!--a.html--> <DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>跨域</title> <script> function getData() {//iframe載入后執行該函數 var iframe = document.getElementById("proxy"); iframe.onload=function() {//a.html與iframe屬於同源了,可以互相訪問 var data = iframe.contentWindow.name;//獲取iframe里的window.name,也就是b.html頁面給它設置的數據 alert(data); } iframe.src="data.html";//這里的data.html為隨便的一個頁面,目的是,使得a.html能訪問到iframe里的內容,也可設置成about:blank </script> <head> <body> <iframe id="proxy" src="http://baidu.com/b.html" style="display:none" onload="getData()"></iframe> </body> </html>
<!--b.html--> <script> window.name="this is some data you got from b.html"; <script>
六、window.postMessage方法跨域
window.postMessage是一個安全的跨源通信方法。一般情況下,當且僅當執行腳本的頁面使用相同的協議(通常都是http)、相同的端口(http默認80,https默認443)和相同的host(兩個頁面的document.domain的值相同)時,才允許不同頁面上的腳本互相訪問。window.postMessage提供了一個可控的機制來安全地繞過這一限制,當其在正確使用的情況下。window.postMessage解決的不是瀏覽器與服務器之間的交互,解決的是瀏覽器不同窗口之間的通信問題,可以做的就是同步兩個網頁,當然這兩個網頁需要屬於同一個基礎域名。
example 1:
在a頁面打開了一個不同源的b頁面,你想要讓a和b這兩個頁面互相通信,例如,a要訪問b的LocalStorage。又或者,a頁面里的iframe的src是不同源的b頁面,你想要讓a和b兩個頁面互相通信,比如依舊是想通過a訪問b的LocalStorage。
此時的解決辦法是:利用HTML5中新增的跨文檔通信API,這個API為window對象新增了一個window.postMessage方法,允許跨窗口通信,不論這兩個窗口是否同源。a就可以把它的Local Storage發給b,反之,依然可行。
window.postMessage(message, targetOrigin, [transfer])三個參數分別表示為:
- message是向目標窗口發送的數據;
- targetOrigin屬性來指定哪些窗口能收到消息事件,其值可以是字符串“*”(表示無限制)或者一個URI(或者說是發送消息的目標域名);
- transfer可選參數,是一串和message同時傳遞的Transferable對象,這些對象的所有權將被轉移給消息的接收方,而發送一方將不再保有所有權。
另外就是,消息的接收方必須有監聽事件,否則發送消息時就會報錯。
window.addEventListener("message", onmessage); onmessage接收到的message事件包含三個屬性:
data: 從其他window中傳遞過來的數據。
origin: 調用postMessage時消息發送窗口的origin。這個origin不能保證是該窗口的當前或未來origin,因為postMessage被調用后可能被導航到不同的位置。
source: 對發送消息的窗口對象的引用;您可以使用此來在具有不同origin的兩個窗口間建立雙向通信。
//在a頁面執行
var popUp = window.open('http://localhost:3000', 'title'); popUp.postMessage('Hello World!', 'http://localhost:3000');
同時在http://localhost:3000的頁面里監聽message事件:
window.onload = function() { window.addEventListener('message', onmessage); } function onmessage(event) { if(event.origin == 'http://localhost:8080') {//"發送方a的域名" console.log(event.data);//"Hello World!" } else { console.log(event.data);//"Hello World!"
}
}
我們來看另外一個例子:
//發送端代碼 var domain = "https://localhost", index = 1, target = window.open(`${domain}/postmesssage-target.html`); function send() { setInterval(() => { target.postMessage(`第${index++}次數據發送`, domain); }, 1000); } window.addEventListener("message", (e)=>{ if(e.data === 'ok') send(); else console.log(e.data); }); //接收端代碼 <head> <script> opener.postMessage("ok", opener.domain); </script> </head> <body> <p id = "test"></p> <script> var test = document.querySelector("#test"); window.addEventListener("message", (e)=>{ if(e.origin !== "http://localhost") { return void 0; } test.innerText = e.data; }); </script> </body>
上面頁面中,接受頁面已經加載了,這時發送一個消息給發送端,發送端再開始向接收端發送數據。
七、片段識別符實現跨域
片段識別符就是指URL的#號后面的部分。比如,http://example.com/x.html#fragment的#fragment。如果只是改變片段標識符,頁面不會重復刷新。父窗口和iframe的子窗口之間的通訊或者是window.open打開的子窗口之間的通訊。
父窗口可以把信息,寫入子窗口的片段標識符。
var src= originURL + '#' + data; document.getElementById('myIFrame').src = src;
子窗口通過監聽hashchange事件得到通知。
window.onhashchange = checkMessage; function checkMessage() { var message = window.location.hash; //... }
同樣,子窗口也可以改變父窗口的片段標識符。
parent.location.href = target + '#' + hash;
總之,父窗口改變子窗口的url的#號后面的部分,后者把要傳遞的的參數寫在#后面,子窗口監聽window.onhashchange事件,得到通知,讀取window.location.hash解析出有用的數據。同時子窗口也可以向父窗口傳遞數據。
參考: