有關(guān)跨域的相關(guān)問(wèn)題和方法

跨域是什么

同源策略

在講解什么是跨域之前先要清楚什么是同源策略,“同源政策”(same-origin policy)是瀏覽器安全的基石。下面將講解同源策略的相關(guān)概念。

1.1 含義

1995年,同源政策由 Netscape 公司引入瀏覽器。目前,所有瀏覽器都實(shí)行這個(gè)政策。

最初,它的含義是指,A網(wǎng)頁(yè)設(shè)置的 Cookie,B網(wǎng)頁(yè)不能打開(kāi),除非這兩個(gè)網(wǎng)頁(yè)“同源”。所謂“同源”指的是”三個(gè)相同“,即:

協(xié)議相同
域名相同
端口相同

1.2 目的

同源政策的目的,是為了保證用戶信息的安全,防止惡意的網(wǎng)站竊取數(shù)據(jù)。

設(shè)想這樣一種情況:A網(wǎng)站是一家銀行,用戶登錄以后,又去瀏覽其他網(wǎng)站。如果其他網(wǎng)站可以讀取A網(wǎng)站的 Cookie,會(huì)發(fā)生什么?

很顯然,如果 Cookie 包含隱私(比如存款總額),這些信息就會(huì)泄漏。更可怕的是,Cookie 往往用來(lái)保存用戶的登錄狀態(tài),如果用戶沒(méi)有退出登錄,其他網(wǎng)站就可以冒充用戶,為所欲為。因?yàn)闉g覽器同時(shí)還規(guī)定,提交表單不受同源政策的限制。

由此可見(jiàn),“同源政策”是必需的,否則 Cookie 可以共享,互聯(lián)網(wǎng)就毫無(wú)安全可言了。

1.3 限制范圍

隨著互聯(lián)網(wǎng)的發(fā)展,“同源政策”越來(lái)越嚴(yán)格。目前,如果非同源(也稱(chēng)非本域),共有三種行為受到限制:

(1) Cookie、LocalStorage 和 IndexedDB 無(wú)法讀取。
(2) DOM 無(wú)法獲得。
(3) AJAX 請(qǐng)求不能發(fā)送。

雖然這些限制是必要的,但是有時(shí)很不方便,合理的用途也受到影響。下面,我將詳細(xì)介紹,如何規(guī)避上面三種限制。

如何實(shí)現(xiàn)跨域

1. JSONP實(shí)現(xiàn)跨域

JSONP是服務(wù)器與客戶端跨源通信的常用方法。最大特點(diǎn)就是簡(jiǎn)單適用,老式瀏覽器全部支持,服務(wù)器改造非常小。

它的基本思想是,網(wǎng)頁(yè)通過(guò)添加一個(gè)<script>元素,向服務(wù)器請(qǐng)求JSON數(shù)據(jù),這種做法不受同源政策限制;服務(wù)器收到請(qǐng)求后,將數(shù)據(jù)放在一個(gè)指定名字的回調(diào)函數(shù)里傳回來(lái)。
首先,網(wǎng)頁(yè)動(dòng)態(tài)插入<script>元素,由它向跨源網(wǎng)址發(fā)出請(qǐng)求。

 $('.change').addEventListener('click', function(){
    var script = document.createElement('script');
    script.src = 'http://localhost:8080/getNews?callback=appendHtml';
    document.head.appendChild(script);
    document.head.removeChild(script);
  })

上面代碼通過(guò)動(dòng)態(tài)添加<script>元素,向服務(wù)器localhost:8080發(fā)出請(qǐng)求。注意,該請(qǐng)求的查詢(xún)字符串有一個(gè)callback參數(shù),用來(lái)指定回調(diào)函數(shù)的名字,這對(duì)于JSONP是必需的。
讓我們看看后臺(tái)是怎么處理請(qǐng)求的:

var cb = req.query.callback;
    if(cb){
        res.send(cb + '('+ JSON.stringify(data) + ')');
    }else{
        res.send(data);
    }

服務(wù)器收到這個(gè)請(qǐng)求以后,如果發(fā)現(xiàn)請(qǐng)求的query中有先前約定好的callback參數(shù),就會(huì)將后臺(tái)數(shù)據(jù)放在回調(diào)函數(shù)callback的參數(shù)位置返回,否則直接返回后臺(tái)數(shù)據(jù)。注意返回的是字符串。
在前端的HTML里我們已經(jīng)定義了callback()函數(shù)如下:(callabck=appendHtml)

function appendHtml(news){
    var html = '';
    for( var i=0; i<news.length; i++){
      html += '<li>' + news[i] + '</li>';
    }
    console.log(html);
    $('.news').innerHTML = html;
  }

如此一來(lái),后臺(tái)的數(shù)據(jù)就會(huì)展現(xiàn)在前端頁(yè)面,實(shí)現(xiàn)了跨域訪問(wèn)數(shù)據(jù)。

2. CORS實(shí)現(xiàn)跨域

CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫(xiě)。它是W3C標(biāo)準(zhǔn),是跨源AJAX請(qǐng)求的根本解決方法。相比JSONP只能發(fā)GET請(qǐng)求,CORS允許任何類(lèi)型的請(qǐng)求。

2.1 簡(jiǎn)介

CORS需要瀏覽器和服務(wù)器同時(shí)支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10。
整個(gè)CORS通信過(guò)程,都是瀏覽器自動(dòng)完成,不需要用戶參與。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),CORS通信與同源的AJAX通信沒(méi)有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn)AJAX請(qǐng)求跨源,就會(huì)自動(dòng)添加一些附加的頭信息,有時(shí)還會(huì)多出一次附加的請(qǐng)求,但用戶不會(huì)有感覺(jué)。因此,實(shí)現(xiàn)CORS通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實(shí)現(xiàn)了CORS接口,就可以跨源通信。
后臺(tái)的實(shí)現(xiàn):

    res.header("Access-Control-Allow-Origin", "*"); 
    res.send(data);

上面的代碼在響應(yīng)頭添加了Access-Control-Allow-Origin:*,讓所有網(wǎng)站可以訪問(wèn)該網(wǎng)站后臺(tái)的數(shù)據(jù)。

2.2 兩種請(qǐng)求

瀏覽器將CORS請(qǐng)求分成兩類(lèi):簡(jiǎn)單請(qǐng)求(simple request)和非簡(jiǎn)單請(qǐng)求(not-so-simple request)。

只要同時(shí)滿足以下兩大條件,就屬于簡(jiǎn)單請(qǐng)求。

(1)請(qǐng)求方法是以下三種方法之一:

HEAD
GET
POST

(2)HTTP的頭信息不超出以下幾種字段:

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三個(gè)值application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同時(shí)滿足上面兩個(gè)條件,就屬于非簡(jiǎn)單請(qǐng)求。

瀏覽器對(duì)這兩種請(qǐng)求的處理,是不一樣的。

2.3 簡(jiǎn)單請(qǐng)求的處理

對(duì)于簡(jiǎn)單請(qǐng)求,瀏覽器直接發(fā)出CORS請(qǐng)求。具體來(lái)說(shuō),就是在頭信息之中,增加一個(gè)Origin字段。

下面是一個(gè)例子,瀏覽器發(fā)現(xiàn)這次跨源AJAX請(qǐng)求是簡(jiǎn)單請(qǐng)求,就自動(dòng)在頭信息之中,添加一個(gè)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字段用來(lái)說(shuō)明,本次請(qǐng)求來(lái)自哪個(gè)源(協(xié)議 + 域名 + 端口)。服務(wù)器根據(jù)這個(gè)值,決定是否同意這次請(qǐng)求。

如果Origin指定的源,不在許可范圍內(nèi),服務(wù)器會(huì)返回一個(gè)正常的HTTP回應(yīng)。瀏覽器發(fā)現(xiàn),這個(gè)回應(yīng)的頭信息沒(méi)有包含Access-Control-Allow-Origin字段(詳見(jiàn)下文),就知道出錯(cuò)了,從而拋出一個(gè)錯(cuò)誤,被XMLHttpRequest的onerror回調(diào)函數(shù)捕獲。注意,這種錯(cuò)誤無(wú)法通過(guò)狀態(tài)碼識(shí)別,因?yàn)镠TTP回應(yīng)的狀態(tài)碼有可能是200。

如果Origin指定的域名在許可范圍內(nèi),服務(wù)器返回的響應(yīng),會(huì)多出幾個(gè)頭信息字段:

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

上面的頭信息之中,有三個(gè)與CORS請(qǐng)求相關(guān)的字段,都以Access-Control-開(kāi)頭。

(1)Access-Control-Allow-Origin

該字段是必須的。它的值要么是請(qǐng)求時(shí)Origin字段的值,要么是一個(gè)*,表示接受任意域名的請(qǐng)求。

(2)Access-Control-Allow-Credentials

該字段可選。它的值是一個(gè)布爾值,表示是否允許發(fā)送Cookie。默認(rèn)情況下,Cookie不包括在CORS請(qǐng)求之中。設(shè)為true,即表示服務(wù)器明確許可,Cookie可以包含在請(qǐng)求中,一起發(fā)給服務(wù)器。這個(gè)值也只能設(shè)為true,如果服務(wù)器不要瀏覽器發(fā)送Cookie,刪除該字段即可。

(3)Access-Control-Expose-Headers

該字段可選。CORS請(qǐng)求時(shí),XMLHttpRequest對(duì)象的getResponseHeader()方法只能拿到6個(gè)基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必須在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
相對(duì)JSONP而言,CORS與其使用目的相同,但是比JSONP更強(qiáng)大。
JSONP只支持GET請(qǐng)求,CORS支持所有類(lèi)型的HTTP請(qǐng)求。JSONP的優(yōu)勢(shì)在于支持老式瀏覽器,以及可以向不支持CORS的網(wǎng)站請(qǐng)求數(shù)據(jù)。

tip:對(duì)于非簡(jiǎn)單請(qǐng)求(比如:請(qǐng)求方法是PUT或DELETE,或者Content-Type字段的類(lèi)型是application/json。可以在阮一峰JS標(biāo)準(zhǔn)參考教程中查看

3. iframe

iframe元素可以在當(dāng)前網(wǎng)頁(yè)之中,嵌入其他網(wǎng)頁(yè)。每個(gè)iframe元素形成自己的窗口,即有自己的window對(duì)象。iframe窗口之中的腳本,可以獲得父窗口和子窗口。但是,只有在同源的情況下,父窗口和子窗口才能通信;如果跨域,就無(wú)法拿到對(duì)方的DOM。

比如,父窗口運(yùn)行下面的命令,如果iframe窗口不是同源,就會(huì)報(bào)錯(cuò)。

document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

上面命令中,父窗口想獲取子窗口的DOM,因?yàn)榭缬驅(qū)е聢?bào)錯(cuò)。

反之亦然,子窗口獲取主窗口的DOM也會(huì)報(bào)錯(cuò)。

window.parent.document.body
// 報(bào)錯(cuò)

這種情況不僅適用于iframe窗口,還適用于window.open方法打開(kāi)的窗口,只要跨域,父窗口與子窗口之間就無(wú)法通信。

3.1 降域

如果兩個(gè)窗口一級(jí)域名相同,只是二級(jí)域名不同,那么設(shè)置document.domain屬性,就可以規(guī)避同源策略,拿到DOM,把這種方法也叫做降域。
舉例來(lái)說(shuō),A網(wǎng)頁(yè)是//a.jirengu.com/a.html,B網(wǎng)頁(yè)是//b.jirengu.com/b.html,那么只要設(shè)置相同的document.domain,兩個(gè)網(wǎng)頁(yè)就可以相互訪問(wèn)數(shù)據(jù)。
在兩個(gè)網(wǎng)站對(duì)應(yīng)的HTML在都要設(shè)置:

document.domain = "jrg.com"

tip:document.domain還可以使兩個(gè)一級(jí)域名相同,只是二級(jí)域名不同的網(wǎng)站共享 Cookie。

3.2 對(duì)于完全不同源的網(wǎng)站,目前有兩種方法,可以解決跨域窗口的通信問(wèn)題:

片段識(shí)別符(fragment identifier)
跨文檔通信API(Cross-document messaging)

a)片段標(biāo)識(shí)符(fragment identifier)指的是,URL的#號(hào)后面的部分,比如http://example.com/x.html#fragment的#fragment。如果只是改變片段標(biāo)識(shí)符,頁(yè)面不會(huì)重新刷新。

父窗口可以把信息,寫(xiě)入子窗口的片段標(biāo)識(shí)符:

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

子窗口通過(guò)監(jiān)聽(tīng)hashchange事件得到通知:

window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}

同樣的,子窗口也可以改變父窗口的片段標(biāo)識(shí)符:

parent.location.href= target + “#” + hash;

b)window.postMessage
上面兩種方法都屬于破解,HTML5為了解決這個(gè)問(wèn)題,引入了一個(gè)全新的API:跨文檔通信 API(Cross-document messaging)。

這個(gè)API為window對(duì)象新增了一個(gè)window.postMessage方法,允許跨窗口通信,不論這兩個(gè)窗口是否同源。

舉例來(lái)說(shuō),父窗口aaa.com向子窗口bbb.com發(fā)消息,調(diào)用postMessage方法就可以了:

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');

postMessage方法的第一個(gè)參數(shù)是具體的信息內(nèi)容,第二個(gè)參數(shù)是接收消息的窗口的源(origin),即“協(xié)議 + 域名 + 端口”。也可以設(shè)為*,表示不限制域名,向所有窗口發(fā)送。

子窗口向父窗口發(fā)送消息的寫(xiě)法類(lèi)似:

window.opener.postMessage('Nice to see you', 'http://aaa.com');

父窗口和子窗口都可以通過(guò)message事件,監(jiān)聽(tīng)對(duì)方的消息:

window.addEventListener('message', function(e) {
  console.log(e.data);
},false);

message事件的事件對(duì)象event,提供以下三個(gè)屬性:

event.source:發(fā)送消息的窗口
event.origin: 消息發(fā)向的網(wǎng)址
event.data: 消息內(nèi)容
下面的例子是,子窗口通過(guò)event.source屬性引用父窗口,然后發(fā)送消息:

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*');
}

上面代碼有幾個(gè)地方需要注意。首先,receiveMessage函數(shù)里面沒(méi)有過(guò)濾信息的來(lái)源,任意網(wǎng)址發(fā)來(lái)的信息都會(huì)被處理。其次,postMessage方法中指定的目標(biāo)窗口的網(wǎng)址是一個(gè)星號(hào),表示該信息可以向任意網(wǎng)址發(fā)送。通常來(lái)說(shuō),這兩種做法是不推薦的,因?yàn)椴粔虬踩?,可能?huì)被惡意利用。
event.origin屬性可以過(guò)濾不是發(fā)給本窗口的消息:

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  if (event.origin !== 'http://aaa.com') return;
  if (event.data === 'Hello World') {
      event.source.postMessage('Hello', event.origin);
  } else {
    console.log(event.data);
  }
}

小結(jié)

本文介紹了同源策略,跨域的概念,簡(jiǎn)單介紹了JSONP、CROS以及iframe使用場(chǎng)景下通過(guò)降域、片段標(biāo)識(shí)符、以及postMessage API來(lái)實(shí)現(xiàn)跨域的方法,讓我們對(duì)跨域的實(shí)現(xiàn)有了較為全面的認(rèn)識(shí)。本文的例子來(lái)自饑人谷官方視頻教程阮一峰JS標(biāo)準(zhǔn)參考教程(alpha)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 題目1.什么是同源策略? 同源策略(Same origin Policy): 瀏覽器出于安全方面的考慮,只允許與本...
    FLYSASA閱讀 1,876評(píng)論 0 6
  • 什么是跨域 跨域,是指瀏覽器不能執(zhí)行其他網(wǎng)站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對(duì)JavaScript實(shí)...
    Yaoxue9閱讀 1,412評(píng)論 0 6
  • 什么是跨域 跨域,是指瀏覽器不能執(zhí)行其他網(wǎng)站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對(duì)JavaScript實(shí)...
    他方l閱讀 1,134評(píng)論 0 2
  • 1. 什么是跨域 跨域,是指瀏覽器不能執(zhí)行其他網(wǎng)站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對(duì)JavaScri...
    cbw100閱讀 6,479評(píng)論 2 86
  • 什么是跨域 跨域,是指瀏覽器不能執(zhí)行其他網(wǎng)站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對(duì)JavaScript實(shí)...
    HeroXin閱讀 955評(píng)論 0 4

友情鏈接更多精彩內(nèi)容