AJAX學習筆記2:XHR實現跨域資源共享(CORS)以及和JSONP的對比

1 前言:

首先對參考文章做者表示感謝,大家的經驗總結給咱們這些新手提供了太多資源。
本文致力於解決AJAX的CORS問題,我在邏輯上進行了梳理:首先,系統的總結了CORS問題的起源---同源策略;其次,介紹JSONP這種僅能支持GET請求的跨域方式和CORS做對比;最後,闡述CORS的XHR解決方式和IE中的XDR解決方式,在此基礎上提供了工具函數進行跨瀏覽器的HTTP請求對象建立。javascript


2 跨域問題的源頭---同源策略

在客戶端編程語言中,如javascript和 ActionScript,同源策略是一個很重要的安全理念,它的目的是爲了保證用戶信息的安全,防止惡意的網站竊取數據。
設想這樣一種狀況:A網站是一家銀行,用戶登陸之後,又去瀏覽其餘網站。若是其餘網站能夠讀取A網站的 Cookie,會發生什麼?html

很顯然,若是 Cookie 包含隱私(好比存款總額),這些信息就會泄漏。更可怕的是,Cookie 每每用來保存用戶的登陸狀態,若是用戶沒有退出登陸,其餘網站就能夠冒充用戶,隨心所欲。由於瀏覽器同時還規定,提交表單不受同源政策的限制。因而可知,"同源政策"是必需的,不然 Cookie 能夠共享,互聯網就毫無安全可言了。前端

那麼什麼叫相同域(同源),什麼叫不一樣的域(不一樣源)呢?當兩個域具備相同的協議(如http), 相同的端口(如80),相同的host(如www.example.org),那麼咱們就能夠認爲它們是相同的域。好比 http://www.example.org/index....(默認端口號80能夠省略)和http://www.example.org/sub/in...是同域,而http://www.example.org, https://www.example.org, http://www.example.org:8080, http://sub.example.org中的任何兩個都將構成跨域。
注意:只有協議、域名、端口號徹底同樣纔是同一域,其餘狀況,即便是相對應的IP和域名也是不一樣域,具體狀況以下圖:
圖片描述
(這個圖片忘了從哪裏引得了,感謝做者)java

目前,若是非同源,共有三種行爲受到限制。編程

(1) Cookie、LocalStorage 和 IndexDB 沒法讀取。
(2) DOM 沒法得到。
(3) AJAX 請求不能發送。

做爲前端開發者,咱們不少時候要作的是突破這種限制。api

補充:同源策略還應該對一些特殊狀況作處理,好比限制file協議下腳本的訪問權限。本地的HTML文件在瀏覽器中是經過file協議打開的,若是腳本能經過file協議訪問到硬盤上其它任意文件,就會出現安全隱患,目前IE8還有這樣的隱患。跨域

3 跨域方式JSONP[參考1]

JSONP是JSON with Padding的簡寫,是應用JSON實現服務器與客戶端跨源通訊的經常使用方法。最大特色就是簡單適用,老式瀏覽器所有支持,服務器改造很是小。瀏覽器

它的基本思想是,網頁經過添加一個<script>元素,向服務器請求JSON數據,這種作法不受同源政策限制;服務器收到請求後,將數據放在一個指定名字的回調函數裏傳回來。
JSOP包含兩部分:回調函數和數據,回調函數是在響應到來時應該調用的函數,通常經過查詢字符串添加;數據就是傳入回調函數中的JSON數據,確切的說,是一個JSON對象,能夠直接訪問。安全

實例:服務器

//訪問跨域src並將數據填入到script標籤中的函數
    function addScriptTag(src) {
      var script = document.createElement('script');
      script.setAttribute("type","text/javascript");
      script.src = src;
      document.body.appendChild(script);
    }
//網頁動態插入<script>元素,由它向跨源網址src發出請求,src中包含回調函數
    window.onload = function () {
      addScriptTag('http://example.com/ip?callback=foo');
    }
//回調函數的參數默認是返回的數據
    function foo(data) {
      console.log('Your public IP address is: ' + data.ip);
    };

/上面代碼經過動態添加<script>元素,向服務器example.com發出請求。注意,該請求的查詢字符串有一個callback參數,用來指定回調函數的名字,這對於JSONP是必需的。/

JSONP的缺點
只能實現GET,沒有POST;從其餘域中加載的代碼可能不安全;難以肯定JSONP請求是否失敗(XHR有error事件),常見作法是使用定時器指定響應的容許時間,超出時間認爲響應失敗。CORS與JSONP的使用目的相同,可是比JSONP更強大,它支持全部類型的HTTP請求。JSONP的優點在於支持老式瀏覽器,以及能夠向不支持CORS的網站請求數據。

4 AJAX實現跨域[參考2]

參考[2]中介紹了複雜HTTP請求,本文只包含簡單的GET和POST請求。
CORS,即Cross-Origin Resource Sharinghttps://www.w3.org/TR/cors/),跨域資源共享,定義了在必須跨域訪問資源時,瀏覽器怎樣和服務器交互。基本思想就是使用自定義的HTTP頭部讓瀏覽器和服務器進行溝通,從而決定響應的成功與失敗。所以瞭解XHR的跨域必需要了解HTTP頭部。

4.1 HTTP header

HTTP header分爲請求頭部和響應頭部,在發送XHR請求時,會發送如下請求頭部:
 Accept:瀏覽器可以處理的內容類型。
 Accept-Charset:瀏覽器可以顯示的字符集。
 Accept-Encoding:瀏覽器可以處理的壓縮編碼。
 Accept-Language:瀏覽器當前設置的語言。
 Connection:瀏覽器與服務器之間鏈接的類型。
 Cookie:當前頁面設置的任何 Cookie。
 Host:發出請求的頁面所在的域 。
 Referer:發出請求的頁面的 URI。注意, HTTP 規範將這個頭部字段拼寫錯了,而爲保證與規範一致,也只能將錯就錯了。(這個英文單詞的正確拼法應該是 referrer。)
 User-Agent:瀏覽器的用戶代理字符串。
Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain

使用 setRequestHeader()方法能夠設置自定義的請求頭部信息。這個方法接受兩個參數:頭部字段的名稱和頭部字段的值。要成功發送請求頭部信息,必須在調用 open()方法以後且調用 send()方法以前調用 setRequestHeader()。
request.open("GET", "https://sf-static.b0.upaiyun.com/v-57fcb48b/user/script/index.min.js", true);
xhr.setRequestHeader("MyHeader", "MyValue");
request.send(null);
服務器在接收到這種自定義的頭部信息以後,能夠執行相應的後續操做。

調用 XHR 對象的 getResponseHeader()方法並傳入頭部字段名稱,能夠取得相應的響應頭部信息。而調用 getAllResponseHeaders()方法則能夠取得一個包含全部頭部信息的長字符串,這種格式化的輸出能夠方便咱們檢查響應中全部頭部字段的名稱。

測試實例:

//建立請求對象
  var request = createRequest();
  if (request == null) {
    alert("Unable to create request");
    return;
  }
  request.onreadystatechange = showSchedule;
  //使用DOM0級方法添加event handler,由於不是全部瀏覽器都支持DOM2;沒有event對象,直接使用request對象
  request.open("GET", selectedTab + ".html", true);//返回HTML片斷
  request.send(null);
}
function showSchedule() {
  if (request.readyState == 4) {
    if ((request.status >= 200 && request.status <= 300)|| request.status == 304) {
    //返回響應頭部
    document.getElementById("content").innerHTML = request.getAllResponseHeaders();
    }else
    {
      document.getElementById("content").innerHTML =request.status;
    }
  }
}

輸出結果顯示所有的響應頭部:
last-modified: Tue, 11 Oct 2016 09:44:48 GMT content-type: application/x-javascript cache-control: max-age=2592000 expires: Thu, 10 Nov 2016 09:45:10 GMT

4.2 CORS 涉及的頭部

IE10及以上、Firefox 3.5+、 Safari 4+、 Chrome、 iOS 版 Safari 和 Android 平臺中的 WebKit 都經過 XMLHttpRequest對象實現了對 CORS 的原生支持。只要在open()方法的URL中使用絕對定位便可實現CORS。通常推薦在同域中使用相對URL,在跨域時使用絕對URL。整個CORS通訊過程,都是瀏覽器自動完成,不須要用戶參與。對於開發者來講,CORS通訊與同源的AJAX通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感受。所以,實現CORS通訊的關鍵是服務器。只要服務器實現了CORS接口,就能夠跨源通訊。

對於簡單請求(GET和POST),瀏覽器直接發出CORS請求。具體來講,就是在頭信息之中,增長一個Origin字段用來講明:本次請求來自哪一個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否贊成此次請求。
若是Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。注意,這種錯誤沒法經過狀態碼識別,由於HTTP迴應的狀態碼有多是200。
若是Origin指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段,以下:

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

上面的頭信息之中,有三個與CORS請求相關的字段,都以Access-Control-開頭。詳細解釋以下:
(1)Access-Control-Allow-Origin
該字段是必須的。它的值要麼是請求時Origin字段的值,要麼是一個*,表示接受任意域名的請求。
(2)Access-Control-Allow-Credentials
該字段可選。它的值是一個布爾值,表示是否容許發送Cookie。默認狀況下,Cookie不包括在CORS請求之中。設爲true,即表示服務器明確許可,Cookie能夠包含在請求中,一塊兒發給服務器。這個值也只能設爲true,若是服務器不要瀏覽器發送Cookie,刪除該字段便可。
(3)Access-Control-Expose-Headers
該字段可選。CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其餘字段,就必須在Access-Control-Expose-Headers裏面指定。上面的例子指定,getResponseHeader('FooBar')能夠返回FooBar字段的值。

4.3 測試實例

把上例中的open()方法改爲以下所示,URL指向其餘域中的一個JavaScript文件,測試結果代表能夠成功加載該文件。
//測試文件在本地主機localhost上
request.open("GET", "https://sf-static.b0.upaiyun.com/v-57fcb48b/user/script/index.min.js"/selectedTab + ".html"/, true);

使用FireFox的FireBug觀察上述請求的響應過程,對應的請求頭部:Origin是http://localhost

響應頭部:access-control-allow-origin:*表明任意源均可以訪問。

跨域使用的XHR對象能夠訪問status和statusText屬性,並且支持同步請求,雖然這沒多大用處。限制是不能使用 setRequestHeader()設置自定義頭部;不能發送和接收 cookie;調用 getAllResponseHeaders()方法總會返回空字符串。

5 特殊的IE: XDR對象實現跨域

對於XHR2,IE瀏覽器的支持是IE10以上。可是IE早在IE8時就推出了 XDomainRequest 對象進行跨域操做,一直沿用到IE10才被取代掉。所以在IE8,IE9中應該使用 XDomainRequest(XDR)來實現。XDR有如下幾個特色:
1)cookie 不會隨請求發送,也不會隨響應返回。(和跨域XHR同樣)
2)只能設置請求頭部信息中的 Content-Type 字段。
3)不能訪問響應頭部信息。(和跨域XHR同樣)
4)只支持 GET 和 POST 請求。

XDR 對象的使用方法與 XHR 對象很是類似,也是建立一個 XDomainRequest 的實例,調用 open()方法,再調用 send()方法。但與 XHR 對象的 open()方法不一樣, XDR 對象的 open()方法只接收兩個參數:請求的類型和 URL。
全部 XDR 請求都是異步執行的,不能用它來建立同步請求(和XHR不一樣同)。請求返回以後,會觸發 load 事件(和XHR同),若是失敗(包括響應中缺乏 Access-Control-Allow-Origin 頭部)就會觸發 error 事件。響應的數據也會保存在 responseText 屬性中。

var xdr = new XDomainRequest();
xdr.onload = function(){
alert(xdr.responseText);
};
xdr.open("get", "http://www.somewhere-else.com/page/");
xdr.send(null);

在請求返回前調用 abort()方法能夠終止請求:

xdr.abort(); //終止請求

與 XHR 同樣, XDR 對象也支持 timeout 屬性以及 ontimeout 事件處理程序
爲支持 POST 請求, XDR 對象提供了 contentType 屬性,用來表示發送數據的格式。該屬性能夠影響頭部信息,在open()以後,send()以前使用。這個屬性是經過 XDR 對象影響頭部信息的惟一方式

6 跨瀏覽器的跨域解決方案

即便瀏覽器對 CORS 的支持程度並不都同樣,但全部瀏覽器都支持簡單的(非 Preflight 和不帶憑據的)請求,所以有必要實現一個跨瀏覽器的方案。檢測 XHR 是否支持 CORS 的最簡單方式,就是檢查是否存在 withCredentials 屬性。再結合檢測 XDomainRequest 對象是否存在,就能夠兼顧全部瀏覽器了。

function createCORSRequest(method, url){
  var xhr = new XMLHttpRequest();
  if ("withCredentials" in xhr){
    xhr.open(method, url, true);
  } else if (typeof XDomainRequest != "undefined"){
    vxhr = new XDomainRequest();
    xhr.open(method, url);
  } else {
    xhr = null;
  }
  return xhr;
}

Firefox、 Safari 和 Chrome 中的 XMLHttpRequest 對象與 IE 中的 XDomainRequest 對象相似,都提供了共同的屬性/方法以下:
 abort():用於中止正在進行的請求。
 onerror:用於替代 onreadystatechange 檢測錯誤。
 onload:用於替代 onreadystatechange 檢測成功。
 responseText:用於取得響應內容。
 send():用於發送請求。
以上成員都包含在 createCORSRequest()函數返回的對象中,在全部瀏覽器中都能正常使用。

var request = createCORSRequest("get", "http://www.somewhere-else.com/page/");
if (request){
    request.onload = function(){
        //對 request.responseText 進行處理
    };
    request.send();
}
相關文章
相關標籤/搜索