跨域及跨域方法小結

記:在近期的面試問題中,做前端,跨域問題必問,跨域涉及到數據的獲取和交互,在前端來說尤爲重要。而在此之前,每次被問到跨域,只能一知半解的回答只知道jsonp,然而對於jsonp的原理還不是很清晰。以下是對跨域知識的全面梳理,在未涉足的方法,先進行提點一下,後續繼續補充和完善。
一、跨域的原因

這是由於瀏覽器的同源策略,所謂的同源策略就是域名、端口號、協議三者要完全相同,才能被界定爲同源。只要三者有一個不同,如果要進行通信,就會產生跨域問題。
注意點:對於協議和端口造成的跨域問題,“前端”無能爲力
域僅僅是同通過“URL首部”來識別,而不會去嘗試判斷相同的ip地址對應的兩個域是否
在同一個ip上,“URL首部”指window.location.protocol+window.location.host

二、跨域的方法

常見的方法有jsonp、window.domain+iframe、window.name+iframe、location.hash+iframe、
postMessage()+iframe、cors跨域、WebSocket協議跨域、nginx代理跨域等方法,下面一一介 紹他們的實現原理以及適應範圍和侷限性

  1. jsonp跨域
全稱是JSON with Padding,是使用Ajax請求實現不同源的跨域。其基本原理:網頁通過添加一個 <script> 元素,向服務器請求 JSON 數據,這種做法不受同源政策限制;服務器收到請求後,將數據放在一個指定名字的回調函數裏傳回來。
jsonp有兩個參數,JSON數據和回調函數,<script>標籤的src屬性指定一個網址,傳遞一個callback參數,該參數值是一個定義在客戶端的回調函數名。在服務器端請求到的文件會直接作爲代碼運行,轉換爲JSON數據,該JSON數據會作爲參數,傳入客戶端的回調函數,客戶端通過回調函數可以處理服務器響應的數據。
舉個栗子:
當前頁面爲http://a.com/a.html,需要請求http://b.com/test.js
在a.html定義如下:
<script type="javascript">
//定義回調函數
function callback(data){
alert(data.meaasge);
}
</script>
<script type="javascript" src="http://b.com/test.js?callback=callback"></script>
以上就完成了jsonp的跨域請求。

也可以通過jquery封裝好的$.ajax()方法來進行jsonp的請求,請求格式如下:
$.ajax({
type:'GET',
dataType:'jsonp',
jsonpCallback:'callback',
data:{
傳入的其他參數
}
});

原生動態的插入script標籤的實現:
let script = document.createElement("script");
script.src =' http://b.com/test.js?callback=callback';
document.body.appendChild(script);
function callback(data){
console.log(data);
}
由於script標籤,只能用於get請求,也就是說jsonp只能處理get請求的跨域,適用在單向跨域,用來請求數據,不能解決兩個不同域的頁面進行相互通信的問題。

2.document.domain+iframe跨域的實現原理
採用這種方式跨域的要求是主域名必須相同,也就是說除了最底層域名不一樣其他都相同,例如a.hxc.com、b.hxc.com、c.hxc.com三者的主域名都是hxc.com,因此可以採用此方式實現跨域。
注意:document.domain設置成自身或者是更高一級的父域
舉個栗子:
前提假設:a.hxc.com/a.html與b.hxc.com/b.html對應兩個不同的ip服務器

在a.html設置如下:
<iframe id="iframe" src="http://b.hxc.com/b.html" οnlοad="test()"></iframe>
<script type="javascript">
document.domain = 'hxc.com'; //設置主域名
function test(){
alert(document.getElementById('iframe').contentWindow);
}
</script>
雖然在a.html已經設置好了,但是這樣是不會成功的。因爲b.html的主域名還不一樣,因此需要在b.html進行設置:
<script type="javascript">
document.domain = 'hxc.com' ; // 設置主域名
</script>
這樣一個跨域的請求就實現了。

3.window.name+iframe實現跨域原理
這裏主要使用的是window對象的name屬性,該屬性的特徵是:在一個window的生命週期裏,窗口載入的所有頁面都共享一個window.name,每個頁面都有讀寫window.name的權限,window.name會存在於一個窗口中載入的所有頁面中,並不會因爲新頁面的載入而進行重置。
利用以上的特性,我們可以在某個頁面先將數據存在window.name裏,然後在進行頁面跳轉後,去訪問和讀取window.name屬性值,即可達到跨域的目的。由於安全原因,瀏覽器會保證window.name爲string類型。

舉個栗子:
首先在被請求的文件裏設置好window.name的值,被請求的頁面爲
然後在發起請求的http://b.hxc.com/index,html進行相關的設置,利用iframe的標籤的跨域能力,加載了請求的頁面,但是會因爲不同源的原因,而無法操作該頁面的任何屬性,因此在index.html的同級目錄下創建一個空白的代理頁面proxy.html,通過重置src屬性值,這樣就做到了同源。爲了同源的其他方法還有,“about:blank javascript: data: ”這裏面的內容都繼承載入他們頁面的源。
<script type="javascript">

let iframe = document.createElement('iframe');
iframe.style.display = 'none';
var state = 0;

iframe.onload = function(){
if(state === 1){
var data = JSON.parse(iframe.contentWindow.name);
console.log(data);
iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe);
}else if(state === 0){
state = 1;
iframe.contentWindow.location= 'http://b.hxc.com/proxy.html';
}
}
document.body.appendChild(iframe);
</script>
這裏首先iframe要具備跨域的能力,其次window.name屬性值在頁面刷新仍然存在。侷限性是隻能用於get的請求。

4.location.hash+iframe跨域實現原理
因爲父窗口可以對iframe進行URL讀寫,iframe也可以讀寫父窗口的URL,URL有一部分被稱爲hash,就是#號及其後面的字符,它一般用於瀏覽器錨點定位,Server端並不關心這部分,應該說HTTP請求過程中不會攜帶hash,所以這部分的修改不會產生HTTP請求,但是會產生瀏覽器歷史記錄。此方法的原理就是改變URL的hash部分來進行雙向通信。每個window通過改變其他 window的location來發送消息(由於兩個頁面不在同一個域下IE、Chrome不允許修改parent.location.hash的值,所以要藉助於父窗口域名下的一個代理iframe),並通過監聽自己的URL的變化來接收消息。這個方式的通信會造成一些不必要的瀏覽器歷史記錄,而且有些瀏覽器不支持onhashchange事件,需要輪詢來獲知URL的改變,最後,這樣做也存在缺點,諸如數據直接暴露在了url中,數據容量和類型都有限等。
當baidu.com/a.html傳數據到goole.com/b.html:
在a.html設置:
將要傳遞的數據放在data裏
<iframe src="goole.com/b.html#data" ></iframe>
在b.html監聽url的hash值的變化,採用的方法是h5的hashchange()方法
如果hash值有變化,則獲取該值
var newHash = parent.location.hash.substring(1);

當b.html傳遞數據到a.html:
由於兩個頁面不在同一個域下IE、Chrome不允許修改parent.location.hash的值,所以要藉助於父窗口域名下的一個代理iframe
在b.html創建一個隱藏的iframe,其src屬性指向'baidu.com/proxy.html#data',並掛上要傳遞的hash值。
proxy.html監聽url的hash的變化,然後修改a.html的url,因爲此時兩頁面是同源的。
a.html監聽到url變化,則取出相應的hash值。
b.html的相關代碼:
try{
parent.location.hash = 'data';
}catsh(e){
//針對ie和chrom不能直接修改parent.location.hash
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = "http://baidu.com/proxy.html#data";
document.body.appendChild(ifrproxy);
}
proxy.html頁面的相關代碼:
//因爲parent.parent(即baidu.com/a.html)和baidu.com/proxy.html屬於同一個域,所
以可以改變其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);
採用該方法可以實現兩個頁面的互相通信,但是隻能用於get請求。與window.name的方法一樣,利用的都是全局的屬性。

5.postMessage()跨域實現

postMessage()是h5實現的一個API,該方法被調用後,會在所有頁面腳本執行完畢之後向目標窗口派發一個  MessageEvent 消息。
MessageEvent消息有四個屬性需要注意:
message 屬性表示該message 的類型; 
data 屬性爲 window.postMessage 的第一個參數;
origin 屬性表示調用window.postMessage() 方法時調用頁面的當前狀態; 
source 屬性記錄調用 window.postMessage() 方法的窗口信息
語法:
otherWindow.postMessage(message,targetOrigin,[transfer])
otherWindow: 是其他窗口的一個引用,比如iframe的contentWindow屬性,執行window.open返回的窗口對象,或者是命名過的數值索引的window.iframes.
meaasge: 將要發送到其他window的數據,它將會被結構化克隆算法序列化
targetOrigin: 通過該屬性來指定哪個窗口可以接收信息,如果不指定可以值爲“*”或者一個URI.利用這個機制,可以限定接收消息的窗口,如果目標窗口不符合同源策略,則消息就不會被髮送。最好指定明確的值,這樣可以防止第三方惡意截獲數據
transfer(該屬性可選):是一串和message 同時傳遞的 Transferable 對象. 這些對象的所有權將被轉移給消息的接收方,而發送一方將不再保有所有權。
其他window可以監聽傳過來的message:
eg:
window.addEventListener("message",receiveMessage,false);
function receiveMessage(event){
var origin = event.origin || event.originalEvent.origin; if (origin !== "http://example.org:8080") return;
}

message的屬性如下:
data:從其他 window 中傳遞過來的對象。
origin:調用 postMessage  時消息發送方窗口的 origin . 這個字符串由 協議、“://“、域名、“ : 端口號”拼接而成。請注意,這個origin不能保證是該窗口的當前或未來origin,因爲postMessage被調用後可能被導航到不同的位置。
source:對發送消息的窗口對象的引用; 您可以使用此來在具有不同origin的兩個窗口之間建立雙向通信。
這裏涉及到的安全問題,如果不希望從其他網站接受到message,不要爲message設置監聽事件。如果要從其他網站接收message時,要始終驗證origin和sourse屬性值,確保發件人身份
以下是實現的栗子:
a.html的相關代碼: 域名爲http://a.hxc.com/a.html
<iframe id='iframe' src='http://b.hxc.com/b.html' style="display:none"></iframe>
<script type="javascript">
var iframe = document.getElementById('iframe');
iframe.onload = function(){
var data = {name:'arm'};
//向b.html傳送數據
iframe.contentWindow.postMessage(JSON.stringify(data),
'http://b.hxc.com/b.html');
};
//監聽b.html頁面傳過來的數據
window.addEventListener('message',function(event){
//驗證發件人的身份
if(event.origin =='http://b.hxc.com/b.html' &&
event.sourse == 'window.parent'){
alert('data from b:'+event.data);
}
})
</script>
b.html的相關代碼:http://b.hxc.com/b.html
<script type="javascript">
//接收來自a頁面的消息
window.addEventListener('message',function(event){
//驗證發件人身份
if(event.origin =='http://a.hxc.com/a.html' &&
event.sourse == 'iframe.contentWindow'){
alert('data from b:'+event.data);
};

//發送數據
var data = JSON.parse(event.data);
if(data){
data.number = 16;
//處理後返回a頁面
window.parent.postMessage(JSON.stringify(data),
'http://a.hxc.com/a.html');
}
},false);
</script>

6.cors跨域資源共享

允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了Ajax只能同源使用的限制。需要瀏覽器和服務器同時支持,實現的關鍵在服務器,只要服務器實現cors接口,就可以跨源通信。目前IE9以下的需要額外靠XDomainRequest對象來支持cors。
cors有兩種請求,分爲簡單請求和非簡單請求,瀏覽器對這兩種的請求的處理方式是一樣的

簡單請求:
  • 該請求有一下侷限:
  • 請求方式爲:HEAD POST GET
  • http頭信息不超過一下字段:Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(限於三個值:appication/x-www-form-urlencoded、multipart/form-data、text/plain)
瀏覽器直接發送cors請求,在頭信息會添加一個Origin字段,如下:
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0
...
Origin字段說明來自哪個源,即協議、端口和域名,服務器根據這個值,決定是否同意這次請求。
請求失敗:如果Origin值不在服務器的許可範圍內,服務器還是會返回一個正常的HTTP迴應,只不過迴應頭信息裏沒有Access-Control-Allow-Origin這個字段,並且錯誤會被XMLHttpRequest對象的onerror回調函數捕獲。這種錯誤通過HTTP狀態碼無法識別,因爲此時狀態碼很可能是200.
請求成功:響應頭部裏會多出幾個與cors相關的頭信息字段:
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html;
charset=utf-8
其中Access-Control開頭的字段解析:
Access-Control-Allow-Origin:必須,該值是發送cors請求是的Origin值,也可以是“*”,表示接受任何域名請求
Access-Control-Allow-Credential:可選,爲布爾值,表示是否允許發送cookie,默認情況下爲false,cookie不包含在cors請求中。當設置爲true時,表示服務器允許cookie包含在cors請求中。這個值使用時,只能設置爲true,否則不需要刪除即可。
Access-Control-Expose-Headers:可選,cors請求時,XMLHttpRequest對象只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
如果想拿到其他字段,則必須在該字段裏指定。
在發送cors請求時,默認不發送cookie和HTTP認證信息,如果需要把cookie發送到服務器,一方面需要指定Access-Control-Allow-Credential設置爲true
另一方面。開發者在需要在ajax請求中打開withCredential屬性。設置如下:
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端設置是否帶cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText); } };
// jquery $.ajax({
...    
xhrFields: {        withCredentials: true // 前端設置是否帶cookie    },  
 crossDomain: true, // 會讓請求頭中包含跨域的額外信息,但不會含cookie
... });
否則,即使服務器同意發送Cookie,瀏覽器也不會發送。或者,服務器要求設置Cookie,瀏覽器也不會處理。 但是,如果省略withCredentials設置,有的瀏覽器還是會一起發送Cookie。這時,可以顯式關閉withCredentials。
需要注意的是,如果要發送Cookie,Access-Control-Allow-Origin就不能設爲星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用服務器域名設置的Cookie纔會上傳,其他域名的Cookie並不會上傳,且(跨源)原網頁代碼中的document.cookie也無法讀取服務器域名下的Cookie。


非簡單請求:
條件爲:請求方式爲PUT或DELETE,或者Content-Type類型的值爲application/json
非簡單請求在正式cors請求時,會增加一次HTTP查詢請求,稱爲預檢請求(preflighted request)。
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,否則就報錯。
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
瀏覽器發現,這是一個非簡單請求,就自動發出一個"預檢"請求,要求服務器確認可以這樣請求。下面是這個"預檢"請求的HTTP頭信息。
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭信息裏面,關鍵字段是Origin,表示請求來自哪個源。
除了Origin字段,"預檢"請求的頭信息包括兩個特殊字段。
  • Access-Control-Request-Method:該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是PUT。
  • Access-Control-Request-Headers:該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段,上例是X-Custom-Header
預檢請求的迴應
服務器收到"預檢"請求以後,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以後,確認允許跨源請求,就可以做出迴應
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html;
charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
上面的HTTP迴應中,關鍵的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以請求數據。該字段也可以設爲星號,表示同意任意跨源請求。
如果瀏覽器否定了"預檢"請求,會返回一個正常的HTTP迴應,但是沒有任何CORS相關的頭信息字段。這時,瀏覽器就會認定,服務器不同意預檢請求,因此觸發一個錯誤,被XMLHttpRequest對象的onerror回調函數捕獲。控制檯會打印出如下的報錯信息。
服務器迴應的其他CORS相關字段如下:
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Access-Control-Allow-Methods:該字段必需,它的值是逗號分隔的一個字符串,表明服務器支持的所有跨域請求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請求的那個方法。這是爲了避免多次"預檢"請求。
Access-Control-Allow-Headers:如果瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限於瀏覽器在"預檢"中請求的字段。
Access-Control-Allow-Credentials: 該字段與簡單請求時的含義相同。
Access-Control-Max-Age: 該字段可選,用來指定本次預檢請求的有效期,單位爲秒。上面結果中,有效期是20天(1728000秒),即允許緩存該條迴應1728000秒(即20天),在此期間,不用發出另一條預檢請求。
瀏覽器正常請求迴應:
一旦服務器通過了"預檢"請求,以後每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭信息字段。服務器的迴應,也都會有一個Access-Control-Allow-Origin頭信息字段。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
瀏覽器的正常CORS請求。上面頭信息的Origin字段是瀏覽器自動添加的。下面是服務器正常的迴應。
Access-Control-Allow-Origin: http://api.bob.com Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin字段是每次迴應都必定包含的
cors跨域可以處理所有的HTTP方式請求。
7.WebSocket協議的跨域

WebSocket protocol是HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通信,同時允許跨域通訊,是server push技術的一種很好的實現。
原生WebSocket API使用起來不太方便,我們使用Socket.io,它很好地封裝了webSocket接口,提供了更簡單、靈活的接口,也對不支持webSocket的瀏覽器提供了向下兼容。
前端的代碼實現:
<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script> var socket = io('http://www.domain2.com:8080');
// 連接成功處理
socket.on('connect', function() {
// 監聽服務端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg); });
// 監聽服務端關閉
socket.on('disconnect', function() {
console.log('Server socket has closed.'); }); });
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value); };
</script>
node server代碼:

相關的還有node實現跨域nginx反向代理實現跨域,但是我目前還沒看到那麼多,等我後續學習完,繼續補上。



參考了掘金論壇大佬寫的,可以去看大佬文章:https://juejin.im/post/5a2f92c65188253e2470f16d#heading-3



























發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章