Web開發之跨域與跨域資源共享

原文連接:http://www.devsai.com/2016/11/24/talk-CORS/javascript

同源策略(same origin policy)

1995年,同源政策由 Netscape 公司引入瀏覽器。爲了防止某些文檔或腳本加載別的域下的未知內容,防止形成泄露隱私,破壞系統等行爲發生。html

同源策略作了兩種限制:前端

  1. 不能經過ajax的方法其餘腳本中的請求去訪問不一樣源中的文檔。html5

  2. 瀏覽器中不一樣域的框架之間是不能進行js的交互操做的。java

如今全部的可支持javascript的瀏覽器都會使用這個策略。web

怎麼算同源

URL的三部分徹底相同時咱們就能夠稱其爲同源,這三部分是: 協議域名(主機名)端口都相同。ajax

IE 例外

當涉及到同源策略時,Internet Explorer有兩個主要的例外json

授信範圍(Trust Zones):兩個相互之間高度互信的域名,如公司域名(corporate domains),不遵照同源策略的限制。

端口:IE未將端口號加入到同源策略的組成部分之中,所以 http://company.com:81/index.html 和http://company.com/index.html  屬於同源而且不受任何限制。

跨域的幾種解決方法

雖然同源策略頗有必要,但有不少時候咱們仍是須要去請求其餘域的數據,如:調用不一樣業務的數據,而不一樣業務已子域區分;又或者是第三方公用的數據接口等等canvas

因爲各類緣由,咱們須要經過各類方式來請求到不一樣域下的資源。windows

jsonp

jsonp是經過能夠發出跨域請求的script標籤,使javascript可以得到跨域請求的數據,並調用數據。

先看個例子:
文件index.js :

alert(123);

頁面index.html:

...
<script src="./index.js"></script>
...

當加載頁面index.html後,出123內容的彈窗。經過查看index.js的響應體,會發現響應內容就是alert(123)

圖片描述

因此,能夠這麼思考,只要是經過script標籤請求到的內容就會被當作js代碼執行。

是否能夠在script中的地址src不請求js文件,而是請求服務端的接口(即便不在同源下的),那麼返回的內容就能得到到,而且會當成js代碼來執行。(通常的script標籤都會去請求js代碼文件)

再來看下正常的服務端獲取數據接口。

好比:有這麼個接口/getUserInfo/001,經過ajax請求得到此接口數據{"data" : {"name" : "devsai",like:"everything"}}

獲得數據後在ajax中調用showUserInfo(data)來渲染頁面,data就是接口數據。

若是如今用script標籤來請求數據,那麼一樣能夠得到數據,執行返回到的內容,因是json格式的數據,並不會報錯,但也並無卵用。得到接口的數據確定是想作些什麼的。

再想一想,正常ajax請求後的js執行內容showUserInfo(data),拿到數據後,調用了showUserInfo函數。

那麼,用script標籤來請求數據時,返回的內容直接是showUserInfo(data)不就好了,但服務端又不知道咱們到底要執行哪一個函數,即便事先約定了,但後面因某些事要改,那還得告訴服務端,太麻煩了。
若是知道要執行什麼函數就行了。

固然,這是能夠的,改造下接口,以參數的形式把函數名傳給服務端。

<script src="/jsonp/getUserInfo/001?jsonp_fn=showUserInfo"></script>

Response返回的內容一樣須要改造

Response:
    showUserInfo({"data" : {"name" : "devsai",like:"everything"}})

這樣,經過jsonp,去跨域請求接口數據就完成了。
須要注意的是函數名須要掛在window下面,要否則會報函數名未定義。

改變源(origin):經過document.domain與子域之間的跨域通信

例如在demo.devsai.com/index.html頁面裏執行以下內容:

document.domain = 'devsai.com';

執行該語句後,能夠成功經過devsai.com/index.html的同源檢測, 實現數據的通信,
固然document.domain不能隨意設置,只能設置成當前域,或設置成當前域的頂域。

document.domain經常被用於同站但不一樣域的狀況,例如:www.devsai.com,下嵌入了iframe廣告頁面ad.devsai.com,想要實現兩頁面的通信,就須要對兩個頁面都設置document.domain='devsai.com'

window.name

window對象有個name屬性,該屬性有個特徵:即在一個窗口(window)的生命週期內,窗口載入的全部的頁面都是共享一個window.name的,每一個頁面對window.name都有讀寫的權限,window.name是持久存在一個窗口載入過的全部頁面中的,並不會因新頁面的載入而進行重置。

name只能是字符串。

頁面a.html中:

<script>
window.name = 'page name index.html';
setTimeout(function(){
    window.location = 'http://localhost:8080/static/b.html';
}, 2000);
</script>

頁面b.html:

<script>
    alert(window.name);
</script>

再來看看如何讓a.html頁面獲取數據

用data.html做爲請求數據地址:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Data</title>
</head>
<body>
    <script>
    window.name = '{ "name":"devsai","like" : "everything"}'; // 須要傳入 a.html頁面的數據,必須是字符串
    </script>
</body>
</html>

a.html:

...
<iframe src="http://localhost:8080/static/data.html" onload="getData();" frameborder="0" id="iframe_1"></iframe>
<script>
function getData(){
    var iframe = document.getElementById('iframe_1');
    //隱藏iframe
    iframe.setAttribute("width", "0");
    iframe.setAttribute("height", "0");
    iframe.setAttribute("border", "0");
    iframe.setAttribute("style", "width: 0; height: 0; border: none;");
    iframe.onload = function(){
        console.log(iframe.contentWindow.name);
        var data = iframe.contentWindow.name;
        data = JSON.parse(data);//轉成 JSON
        showUserInfo(data); 
    }
    iframe.src = 'about:blank';
}
function showUserInfo(data){
    console.log(data);
    // .....do something
}
</script>
...

當訪問http://127.0.0.1:8080/static/index.html,便能得到來自不一樣域下data.html中的數據。

也能夠作的更完善些,動態的生成iframe請求數據,用完即毀。

....
// 傳入請求數據接口地址和回調函數
function requestData(url,successCB){
    var body = document.getElementsByTagName('body')[0];
    var iframe = document.createElement("iframe");
    iframe.setAttribute("id", "getDataByWindowName");
    iframe.setAttribute("width", "0");
    iframe.setAttribute("height", "0");
    iframe.setAttribute("border", "0");
    iframe.setAttribute("style", "width: 0; height: 0; border: none;");
    iframe.setAttribute("src", url);
    body.appendChild(iframe);
    setTimeout(function(){//防止iframe.src在沒加載前就被替換
        iframe.onload = function(){
            var data = iframe.contentWindow.name;
            if(data){
                data = JSON.parse(data);//轉成 JSON
                successCB && successCB(data);
            }
            iframe.parentNode.removeChild(iframe);
        }
        iframe.src = 'about:blank';
    }, 100);
}

//requestData("http://localhost:8080/static/data.html",showUserInfo);
...

這就是使用window.name來進行跨域。

window.postMessage

window.postMessage方法是html5的新特性之一,
可使用它來向其它的window對象發送消息,無論這個window對象是屬於同源或不一樣源。

經過window.postMessage容許瀏覽器windows, tabs, and iFrames之間跨域通信。

以前寫過一篇關於window.postMessage的,作了詳細的說明+演示頁面+演示代碼,去看看

服務端地址映射

例如一個網站上有各類不一樣的業務,不一樣的業務有其對應的子域。

如:ad.devsai.com;upload.devsai.com;live.devsai.com,分別對應廣告業務,上傳業務,直播業務。

想在www.devsai.com中作交互,或得到數據,便會受跨域影響。

形成跨域的緣由是由於請求數據的源不一樣,那隻要請求的源同樣,便沒有跨域問題了。

這也是能夠辦到的,只須要web服務作下代理,或稱之爲地址映射。

拿Nginx舉例,須要在web服務上作以下配置:

...
lcaotion /ad {
    proxy_pass http://ad.devsai.com
}

location /upload {
    proxy_pass http://upload.devsai.com
}

location /live {
    proxy_pass http://live.devsai.com
}
...

而後就能夠在以www.devsai.com/ad/的方式去調用廣告業務。

CORS跨域資源共享

當一個發起的請求地址與發起該請求自己所在的地址不在同源下時,稱該請求發起了一個跨域的HTTP請求。

有些的跨域請求是被容許的<img>,<script>,<link>圖片,腳本,樣式及其餘資源 ,加載這些數據時即便不在同源下面也一樣被容許,現在的網站一般也會去引用不在同源下的這些資源,如作CDN加速。

但也有些不被容許,正如你們所知,出於安全考慮,瀏覽器會「限制」腳本中發起的跨站請求,好比:XmlHttpRequest。

除了XmlHttpRequest外,還有如下幾種跨域請求作了相應的安全限制。

好比:

1 前面說的iframe,經過設置src能夠發起跨域請求,但對請求到的內容進行操做就不被容許了。如執行iframe.contentWindow.name就會報錯。


2 <img>標籤上crossorigin屬性是一個CORS的配置屬性,目的是爲了容許第三方網站上的圖片(即不在同源上的圖片)可以在canvas中被使用。
若是沒有配置改屬性,又跨域請求了圖片,當調用canvas中的toBlob(), toDataURL(), 或getImageData()方法的時候,會報錯。

var img = new Image,
    canvas = document.createElement("canvas"),
    ctx = canvas.getContext("2d"),
    src = "https://sf-sponsor.b0.upaiyun.com/45751d8fcd71e4a16c218e0daa265704.png"; // insert image url here
img.crossOrigin = "Anonymous";

img.onload = function() {
    console.log(img);
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage( img, 0, 0 );
    localStorage.setItem( "savedImageData", canvas.toDataURL("image/png") );
}
img.src = src;

3 一樣的script也能夠有crossorigin屬性,
script自己沒有跨域問題,否則jsonp就無法用了。但若是請求的不是同源下的js文件,發生錯誤後,沒法經過window.onerror事件捕捉到詳細的信息

例如加載index.js文件,其中a未定義:

var b = a;

同源下的 window.onerror報錯信息

圖片描述

跨域下的 window.onerror報錯信息

圖片描述

經過script標籤上添加crossdomain屬性,並在服務上配置響應頭。

<script src="http://lcoalhost:/static/index.js" type="text/javascript" charset="utf-8" crossdomain></script>

在去看onerror中的報錯信息就和同源下的報錯信息同樣了。


4 Web字體 (CSS 中經過 @font-face 使用跨站字體資源),使用非同源地址,一樣會報錯。

還須要注意的一點是,跨域請求並不是是瀏覽器限制了請求,而是瀏覽器攔截了返回結果。不論是否跨域,請求都會發送到服務端。
但也有特例,有些瀏覽器不容許從HTTPS的域跨域訪問HTTP,好比Chrome和Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求。

解決這類跨域問題的方法就是*CORS*,對於簡單的請求來講,前端這邊都不須要作任何的編碼就能實現跨域請求,
只須要服務端配置響應頭"Access-Control-Allow-Origin:*"。

什麼是CORS

CORS是一個W3C標準,全稱「跨域資源共享」(Cross-origin resource sharing)

跨源資源共享標準經過新增一系列 HTTP 頭,讓服務器能聲明哪些來源能夠經過瀏覽器訪問該服務器上的資源。

CORS服務端設置(Set Response Header)

Access-Control-Allow-Origin

根據Reuqest請求頭中的Origin來判斷該請求的資源是否能夠被共享。

若是Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin字段(該字段的值爲服務端設置Access-Control-Allow-Origin的值)便知出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。此時HTTP的返回碼爲200,因此 這種錯誤沒法經過狀態碼識別。

Access-Control-Allow-Credentials

指定是否容許請求帶上cookies,HTTP authentication,client-side SSL certificates等消息。
如須要帶上這些信息,Access-Control-Allow-Credentials:true並須要在XmlHpptRequest中設置xhr.withCredentials=true

需注意的是,當設置了the credentials flag爲true,那麼Access-Control-Allow-Origin就不能使用"*"

Access-Control-Max-Age

可選字段,指定了一個預請求將緩存多久,在緩存失效前將不會再發送預請求。

Access-Control-Allow-Methods

做爲預請求Response的一部分,指定了真實請求可使用的請求方式。

Access-Control-Allow-Headers

做爲預請求Response的一部分,指定了真實請求可使用的請求頭名稱(header field names)。

CORS兩種請求方式

CORS的有兩種請求方式: 簡單請求(Simple Request) 和 預請求(Prefilght Request)

簡單請求(Simple Request)

只要同時知足如下三大條件,就屬於簡單請求。

a) 請求方式是如下幾種方式之一

* GET
* POST
* HEAD

b) content-type必須是如下幾種之一

* application/x-www-form-urlencoded
* multipart/form-data
* text/plain

c) 不會使用自定義請求頭(相似於 X-Modified 這種)。

預請求(Prefilght Request)

若是不知足簡單請求的三大條件,會在發送正真的請求前,發送個請求方式爲'OPTIONS'的請求,去服務端作檢測,

1) 請求方式不是GET,POST,HEAD

那麼須要在響應HEAD配置容許的請求方式,例如:Access-Control-Allow-Methods:PUT,DELETE

2) 使用自定義請求頭,如x-devsai ,那麼須要在服務端相應配置容許的自定義請求頭:Access-Control-Request-Headers: x-devsai

一旦檢測不經過,瀏覽器就會提示相應的報錯,並不會發生真實的請求。

CORS兼容性

圖片描述

從上圖可只IE11,如下的就不支持CORS了。但實際上再IE8,IE9,IE10中,能夠用XDomainRequest對象代替XmlHttpReuqest,發送跨域請求。

var xdr = new XDomainRequest(); 

xdr.open("get", "http://www.devsai.com/xdr");

xdr.send();

結語

最後,總結下各類跨域方案的特色,還記得本文開始說的,同源策略的兩種限制嗎?

  1. 不能經過__ajax的方法__或__其餘腳本中的請求__去訪問不一樣源中的文檔。

  2. 瀏覽器中不一樣域的框架之間是不能進行js的交互操做的。

把第1種標記爲__TYPE_1__,第二種標記爲__TYPE_2__,對上述的幾種解決跨域的方法分下類。

window.name 須要注意name只能是字符串

解決的限制 :__TYPE_1__,__TYPE_2__

缺點: 接口返回的內容必須都是html裏嵌入script腳本。


document.domain 經過修改domain跨子域

解決的限制 :__TYPE_2__

缺點: 僅支持同個域下的子域跨域,跨域能力有限


window.postMessage 用於iframe、window、tabs之間的跨域通信

解決的限制 :__TYPE_2__

缺點: 兼容問題,IE10如下受限,IE8如下無效


jsonp 是以前最經常使用的解決跨域請求的方法。

解決的限制 :__TYPE_1__

缺點: 不能用於POST請求


服務端地址映射 前端不須要管,並能解決跨域請求問題的一種方法。

解決的限制 :__TYPE_1__

缺點: 非要說缺點,那就是要說服服務端同窗,並且通常場子鋪大了的公司只用同源,不太可能。


CORS 感受目前比較經常使用的解決跨域請求的方法。

解決的限制 :__TYPE_1__

缺點: 也是兼容性問題

真正開發過程當中,需針對不一樣狀況,使用不一樣的解決之法。

相關文章
相關標籤/搜索