1. 程式人生 > >CORS跨域資源共享你該知道的事兒

CORS跨域資源共享你該知道的事兒

自身 沒有 對象的使用方法 one 一起 接受 gpm spa eat

“嘮嗑之前,一些客套話”

CORS跨域資源共享,這個話題大家一定不陌生了,吃久了大轉轉公眾號的深度技術好文,也該吃點兒小米粥溜溜胃裏的縫兒了,今天咱們就再好好屢屢CORS跨域資源共享這個話題,大牛怡情小牛鞏固,把這碗前端經久不涼的大碗茶,再細細的品一品。


“JSONP直接了當很豪爽,CORS細吮慢品大補湯”

在咱們前端的日常工作中,跨域比較常用的方式就是JSONP,JSONP呢就是通過script標簽無同源限制的特點,在獲取到需要的資源後自動執行回調方法的方式,而我們瀏覽器原生的CORS跨域,是通過“正當手段”得到服務器小姐姐首肯,大搖大擺獲取跨域資源的方式,相比JSONP只能實現GET請求,CORS大法支持所有的請求類型,同時CORS是通過普通的XMLHttpRequest發起請求和獲得數據,比起JSONP有更好的錯誤處理,接下來我們就來說下這個CORS大法。


“整體概述,先擺個譜”

官方簡略的: CORS(Cross-Origin Resource Sharing)跨域資源共享,主要思想就是使用自定義的HTTP頭部讓瀏覽器與服務器進行溝通,從而決定響應是成功還是失敗,它允許了瀏覽器向跨源服務器發送請求,從而克服了同源的限制。

私下露骨的: 其實就是向服務器發送跨域請求時,瀏覽器自動針對普通請求和非普通請求進行區別對待,在請求頭中加個Origin字段告訴服務器這個請求的源,通過服務器返回的響應頭中Access-Control-Allow-Origin字段的值是不是請求中的Origin,來看服務器讓不讓咱請求到這資源。


“我是一些工作中不怎麽用得到的基本知識,可我也是一條小生命啊”

CORS 瀏覽器的支持情況 :

技術分享

瀏覽器端已經獲得了良好的支持,所以實現CORS的關鍵就是服務器,只要實現了CORS的接口,就可以實現跨域通信。

IE對CORS的實現

IE8中引入了XDR(XDomainRequest),註意:

  1. cookie不會隨請求發送,也不會隨響應返回

  2. 只能設置請求頭部信息中的Content-Type字段

  3. 不能訪問響應頭部信息

  4. 只支持GET和POST請求

XDR對象的使用方法用戶XHR對象非常類似,如下:

  1.  1 var xdr = new XDomainRequest();
     2 
     3 xdr.onload = function() {
    
    4 5 alert(xdr.responseText); 6 7 } 8 9 xdr.onerror = function() { 10 11 alert("error"); 12 13 } 14 15 xdr.open("get", "http://www.xxx.com/yyy/"); 16 17 xdr.send(null);

其他瀏覽器對CORS的實現

Firefox3.5+,Safari4+,Chorme,IOS版的Safari和Android平臺下的WebKit都通過XmlHttpRequest實現了對CORS的支持。

  1.  1 var xhr = new XMLHttpRequest();
     2 
     3 xhr.onreadystatechange = function () {
     4 
     5    if(xhr.readyState == 4){
     6 
     7        if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
     8 
     9            console.log(xhr.responseText)
    10 
    11        }else {
    12 
    13            console.log(‘err‘ + xhr.status);
    14 
    15        }
    16 
    17    }
    18 
    19 };
    20 
    21 xhr.open(‘get‘,‘http://www.xxx.com/zzz/‘,true);
    22 
    23 xhr.send(null);

跨域XHR一些安全限制:

  1. 不能使用setRequestHeader()設置自定義頭部

  2. 不能發送和接收cookie

  3. 調用獲取所有頭部信息的方法getAllReponseHeaders()方法會返回空字符串


“請求沒有那麽簡單,每種都有她的習慣”

瀏覽器將CORS請求分成兩類。

1.簡單請求:

高大上版定義:

Simple requests

A simple cross-site request is one that meets all the following conditions:

The only allowed methods are: GET HEAD POST

Apart from the headers set automatically by the user agent (e.g. Connection,User-Agent, etc.), the only headers which are allowed to be manually set are:
Accept
Accept-Language
Content-Language
Content-Type

The only allowed values for the Content-Type header are:
application/x-www-form-urlencoded multipart/form-data text/plain

大意就是說:

  1. 請求方法是以下三種方法之一:HEAD,GET,POST

  2. HTTP的頭信息不超出以下幾種字段:Accept,Accept-Language,Content-Language,Last-Event-ID

  3. Content-Type只限於三個值:application/x-www-form-urlencoded、multipart/form-data、text/plain

2.非簡單請求:

非簡單請求是那種對服務器有特殊要求的請求,除以上條件之外的都是非簡單請求,條件如下:

  1. 使用了下面任一 HTTP 方法:PUT,DELETE,CONNECT,OPTIONS ,TRACE ,PATCH

  2. 人為設置了對 CORS 安全的首部字段集合之外的其他首部字段。該集合為:Accept,Accept-Language,Content-Language,Content-Type (but note the additional requirements below),DPR,Downlink,Save-Data,Viewport-Width,Width

  3. Content-Type 的值不屬於下列之一:application/x-www-form-urlencoded,multipart/form-data,text/plain


“跨域請求分兩隊,差別對待也是醉”

在討論"CORS對不同請求的處理"這部分內容時,我們同步跑起來一個nodejs的項目對照著理解,紙上談兵終覺淺,要幹大事還得碼啊!

為了更加直觀,我們為我們接下來要占用的兩個端口配下代理:

  1. 127.0.0.1:8081 m.zhuanzhuan.com

  2. 127.0.0.1:8082 u.58.com

客戶端nodejs腳本 client.js:

  1.  1 var http = require(‘http‘);
     2 
     3 var fs = require(‘fs‘);
     4 
     5 var url = require(‘url‘);
     6 
     7  
     8 
     9 // 創建服務器
    10 
    11 http.createServer( function (request, response) {
    12 
    13  // 解析請求,包括文件名
    14 
    15  var pathname = url.parse(request.url).pathname;
    16 
    17  // 輸出請求的文件名
    18 
    19  console.log("Request for " + pathname + " received.");
    20 
    21  // 讀取請求的文件內容
    22 
    23  fs.readFile(pathname.substr(1), function (err, data) {
    24 
    25    if (err) {
    26 
    27      console.log(err);
    28 
    29      // HTTP 狀態碼: 404 : NOT FOUND Content Type: text/plain
    30 
    31      response.writeHead(404, {‘Content-Type‘: ‘text/html‘});
    32 
    33    }else{
    34 
    35      // HTTP 狀態碼: 200 : OK
    36 
    37      response.writeHead(200, {‘Content-Type‘: ‘text/html‘});
    38 
    39      // 響應文件內容
    40 
    41      response.write(data.toString());
    42 
    43    }
    44 
    45    //  發送響應數據
    46 
    47    response.end();
    48 
    49  });
    50 
    51 }).listen(8082);
    52 
    53 // 控制臺會輸出以下信息
    54 
    55 console.log(‘Server running at http://u.58.com/‘);

服務端nodejs腳本 server.js:

  1.  1 var express = require(‘express‘);
     2 
     3 var app = express();
     4 
     5 var router = express.Router();
     6 
     7  
     8 
     9 router.all(‘/getData‘, function(req, res, next) {
    10 
    11  //設置允許跨域請求
    12 
    13  var reqOrigin = req.header("origin");
    14 
    15   console.log(reqOrigin);
    16 
    17   if(reqOrigin !=undefined &&
    18 
    19   reqOrigin.indexOf("http://u.58.com") > -1){
    20 
    21    //設置允許 http://u.58.com 這個域響應
    22 
    23    res.header("Access-Control-Allow-Origin", "http://u.58.com");
    24 
    25    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    26 
    27    res.header("Authorization",‘zhuanzhuanFe‘)
    28 
    29    res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
    30 
    31  }
    32 
    33  res.json(200, {name:"轉轉熊",property:"Cute"});
    34 
    35 });
    36 
    37  
    38 
    39 app.use(‘/‘, router);
    40 
    41  
    42 
    43 var server = app.listen(8081, function () {
    44 
    45  console.log("應用實例,訪問地址為http://127.0.0.1:8081/");
    46 
    47 });
    48 
    49  
    50 
    51 console.log(‘Server running at http://m.zhuanzhuan.com/‘);



創建一個index.html

  1.  1 <!DOCTYPE html>
     2 
     3 <html lang="zh-CN">
     4 
     5 <head>
     6 
     7  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
     8 
     9  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    10 
    11  <title>test cors</title>
    12 
    13 </head>
    14 
    15 <body >
    16 
    17    <button id="btn" onclick="getData()">跨域獲取數據</button>
    18 
    19 </body>
    20 
    21 <script>
    22 
    23  function getData(){
    24 
    25    var xhr = new XMLHttpRequest();
    26 
    27    xhr.onreadystatechange = function () {
    28 
    29      if(xhr.readyState == 4){
    30 
    31        if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
    32 
    33          console.log(xhr.responseText)
    34 
    35        }else {
    36 
    37          console.log(‘err‘ + xhr.status);
    38 
    39        }
    40 
    41      }
    42 
    43    };
    44 
    45    xhr.open(‘get‘,‘http://m.zhuanzhuan.com/getData‘,true);
    46 
    47    xhr.send(null);
    48 
    49  }
    50 
    51  
    52 
    53 </script>
    54 
    55 </html>

我們在控制臺執行我們創建的client.js

技術分享

在瀏覽器訪問 http://u.58.com/index.html(也就是我們的127.0.0.1:8082),控制臺會看到如下輸出:

技術分享

同時,我們把我們的服務端也跑起來,註意上一個服務不要停掉哈

技術分享

有了代碼心裏就有底了,接下來我們來看下簡單和非簡單請求,CORS到底do了what!

1.簡單請求:

按照上面的getData方法的配置:

  1. xhr.open(‘get‘,‘http://m.zhuanzhuan.com/getData‘,true);

我們點擊一下http://u.58.com/index.html頁面的按鈕,瀏覽器就會從http://u.58.com下發向http://m.zhuanzhuan.com/getData發送一個普通的GET請求

對於簡單的跨域請求,瀏覽器自動的發出CORS請求,在請求頭中,增加一個Origin字段,如下請求頭:

技術分享

請求頭RequestHeaders中有一個Origin字段,這個字段表示本次請求來自哪個源(協議 + 域名 + 端口)

服務器接收到請求後,如果不為這個跨域請求做任何的操作,如下改寫下server.js:

  1. router.all(‘/getData‘, function(req, res, next) {

  2. //不做任何的處理

  3. });

服務器接收到請求後,如果Origin指定的源不在許可範圍內,服務器會返回一個正常的HTTP回應,瀏覽器接收到的回應頭信息中沒有包含Access-Control-Allow-Origin字段,那麽瀏覽器就會拋出一個錯誤,被XHR的onerror函數捕捉,這種情況無法通過狀態碼判斷,狀態碼可能會返回200。

技術分享

如果請求頭中的Origin字段是服務器允許的來源,那麽服務器會在請求的返回頭中添加Access-Control-Allow-Origin字段,並賦值為請求頭中的Origin,表示允許該源請求資源。接下來我們恢復server.js中之前對getData請求的處理,再次點擊按鈕發起跨域請求,模擬一下上述情景:

  1.  1 router.all(‘/getData‘, function(req, res, next) {
     2 
     3  //設置允許跨域請求
     4 
     5  var reqOrigin = req.header("origin");
     6 
     7   console.log(reqOrigin);
     8 
     9   if(reqOrigin !=undefined && reqOrigin.indexOf("http://u.58.com") > -1){
    10 
    11    //設置允許 http://u.58.com 這個域響應
    12 
    13    res.header("Access-Control-Allow-Origin", "http://u.58.com");
    14 
    15    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    16 
    17    res.header("Authorization",‘zhuanzhuanFe‘);
    18 
    19    res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization");
    20 
    21  }
    22 
    23  res.json(200, {name:"轉轉熊",property:"Cute"});
    24 
    25 });

如果Origin指定的域名在許可的範圍內的話,服務器返回的響應,會多出來幾個頭信息字段:

技術分享

響應頭Response Headers中以Access-Control-開頭的字段都是與CORS相關的字段,其中Access-Control-Allow-Origin 該字段是允許跨域響應頭中必不可少的一個字段,值一般都是請求中Origin的值,表示允許當前源的跨域請求,也可以設置成*,表示接受任意源的請求。其余相關字段下面會詳解。

2.非簡單請求 Preflighted Request: 當瀏覽器發送的請求為非簡單請求時,瀏覽器必須首先使用 OPTIONS 方法向服務器發起一個預檢請求(Rreflighted Request),從而獲知服務端是否允許該跨域請求。預檢請求的使用,可以避免跨域請求對服務器的用戶數據產生未預期的影響。

我們將請求從簡單請求GET改成非簡單請求PUT,將index.html中getData方法稍加改動:

  1. xhr.open(‘put‘,‘http://m.zhuanzhuan.com/getData‘,true);

當然,我們還是要先看看對跨域請求不做任何處理的時候的狀態,繼續將server.js中對Access-Control-Allow-Origin字段的設置註釋掉,發送下請求:

技術分享

響應頭中Access-Control-Allow-Origin字段如果不設置為Origin字段的值或者“*”,就表示該預檢請求不被同意,會返回一個正常的HTTP回應,但是沒有任何的CORS相關頭信息字段,這時瀏覽器接收到響應,會被XMLHttpRequest對象的onerror()回調函數捕獲,控制臺會打出如上的報錯信息。

我們接下來看看正常的復雜請求的處理邏輯。恢復server.js中對Access-Control-Allow-Origin字段的設置,刷新頁面,點擊按鈕再次發送請求,我們看到比起我們要發送的PUT請求,頁面多了一次類型為OPTIONS的請求:

技術分享

這個就是預檢請求。對於預檢請求,響應頭中有一個必須的字段:Access-Control-Request-Method,表示請求允許使用的方法,如果沒有這個字段,預檢請求無法通過,我們來實踐一下,註釋掉之前在響應中對Access-Control-Request-Method字段的設置,改寫下server.js:

  1.  1 router.all(‘/getData‘, function(req, res, next) {
     2 
     3  //設置允許跨域請求
     4 
     5  var reqOrigin = req.header("origin");
     6 
     7   if(reqOrigin !=undefined && reqOrigin.indexOf("http://u.58.com") > -1){
     8 
     9    //設置允許 http://u.58.com 這個域響應
    10 
    11    res.header("Access-Control-Allow-Origin", "http://u.58.com");
    12 
    13  }
    14 
    15  res.json(200, {name:"轉轉熊",property:"Cute"});
    16 
    17 });

點擊頁面按鈕發送put請求,可以看到頁面控制臺報錯:

技術分享

顧名思義,我們發送的put方法並沒有在響應頭中設置被允許,所以,預檢請求失敗,真正的請求也就被扼殺在搖籃裏了...

來來來讓我們動氣程序員證就世界的手指把搖籃裏的巨嬰救活吧!恢復server.js到最開始的狀態,重新發送請求:

技術分享

服務器一旦通過了預檢請求,以後每次瀏覽器正常的CORS請求都會跟簡單請求一樣,妥了,救活,萬歲!


“憑證不是你想帶,想帶就能帶”

默認情況下跨域請求不提供憑據(Cookie,HTTP認證以及SSL證明等),但是通過將xhr的withCredentials屬性設置為true,就可以指定某個請求發送憑據。如果服務器接受帶憑據的請求,會在響應頭中用Access-Control-Allow-Credentials:true來響應。

首先我們改寫下index.html中的getData方法:

  1.  1 function getData(){
     2 
     3    var xhr = new XMLHttpRequest();
     4 
     5    //允許跨域攜帶請求
     6 
     7    xhr.withCredentials = true;
     8 
     9    xhr.onreadystatechange = function () {
    10 
    11      if(xhr.readyState == 4){
    12 
    13        if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
    14 
    15          console.log(xhr.responseText)
    16 
    17        }else {
    18 
    19          console.log(‘err‘ + xhr.status);
    20 
    21        }
    22 
    23      }
    24 
    25    };
    26 
    27    xhr.open(‘put‘,‘http://m.zhuanzhuan.com/getData‘,true);
    28 
    29    xhr.send(null);
    30 
    31  }

我們刷新頁面發送請求:

技術分享

一廂情願了吧,意思就是在預檢請求的響應頭中如果不同時為Access-Control-Allow-Credentials設置為true,那麽瀏覽器就不會把響應交給javascript,responseText為空字符串,status為0,觸發onerror(),跨域攜帶憑證的請求是不被允許滴。

我們乖乖在server.js響應中加上下面的代碼:

  1. res.header("Access-Control-Allow-Credentials",‘true‘);

再次請求,可以看到:

技術分享

拖家帶口闖關東成功!

對於附帶身份憑證的請求,服務器不得設置 Access-Control-Allow-Origin 的值為“*”,值必須為Origin 首部字段所指明的域名即允許附帶憑證的源,實踐一下看看,將server.js中稍加修改:

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

發送請求,可以看到:

技術分享

祖國河山一片紅,不說應該也懂了吧,就是不行哈不行,NOOO!


“...我編不出來了...反正就是介紹下雙方出場隊員= =”

最後我們將跨域涉及到的一些響應頭中Access-Control-Allow家族常見的字段羅列下,給我們的知識系統來幾下80塊的紮實大錘:

HTTP 響應首部字段

  1. Access-Control-Allow-Origin: |*: 表示可以請求數據的請求來源

  2. Access-Control-Expose-Headers:zhuanzhuanFe: 在跨域訪問時,XMLHttpRequest對象的getResponseHeader()方法只能拿到一些最基本的響應頭,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要訪問其他頭,則需要服務器設置本響應頭,如上這樣瀏覽器就能夠通過getResponseHeader訪問zhuanzhuanFe響應頭了。

  3. Access-Control-Allow-Credentials: true:允許跨域攜帶憑證的字段

  4. Access-Control-Max-Age: < delta-seconds >:來指定本次預檢請求的有效期,單位為秒,在此期間瀏覽器無需為同一請求再次發送預檢請求。請註意,瀏覽器自身維護了一個最大有效時間,如果該首部字段的值超過了最大有效時間,將不會生效。

  5. Access-Control-Allow-Methods: < method >[, < method >]:首部字段用於預檢請求的響應。其指明了實際請求所允許使用的 HTTP 方法。

  6. Access-Control-Allow-Headers: < field-name >[, < field-name >]*:如果瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,表明服務器支持的所有頭信息字段,不限於瀏覽器在"預檢"中請求的字段。

HTTP 請求首部字段

  1. Origin: < origin >:表明預檢請求或實際請求的源

  2. Access-Control-Request-Method: < method >: 將實際請求所使用的 HTTP 方法告訴服務器。

  3. Access-Control-Request-Headers: < field-name >[, < field-name >]*:將實際請求所攜帶的首部字段告訴服務器


差不多就這麽多了,鞠躬感謝,前端小新人的一些拙劣總結,有問題的地方還希望各位前輩們多多指教,一起攜手在前端的草原策馬奔騰吧!嘚駕~~~~

如果你喜歡我們的文章,關註我們的公眾號和我們互動吧。

技術分享

CORS跨域資源共享你該知道的事兒