Yii2 Restful 跨域調(diào)用 - CORS 詳解

標簽(空格分隔): Yii2


1 什么是CORS

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

它允許瀏覽器向跨源服務器發(fā)出XMLHttpRequest請求,從而繞過AJAX的同源策略(Same-origin Policy)
那么什么是Same-origin Policy呢?簡單地說,在一個瀏覽器中訪問的網(wǎng)站不能訪問另一個網(wǎng)站中的數(shù)據(jù),除非這兩個網(wǎng)站具有相同的Origin,也即是擁有相同的協(xié)議、主機地址以及端口。一旦這三項數(shù)據(jù)中有一項不同,那么該資源就將被認為是從不同的Origin得來的,進而不被允許訪問。

2 CORS 請求分類

瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。

只要同時滿足以下兩大條件,就屬于簡單請求。

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

2 HTTP 的頭信息不超出以下幾種字段:  
    Accept  
    Accept-Language 
    Content-Language  
    Content-Type (只限于三個值 application/x-www-form-urlencoded | multipart/form-data | text/plain )
    Last-Event-ID  

凡是不同時滿足上面兩個條件,就屬于非簡單請求。

瀏覽器對這兩種請求的處理,是不一樣的。

3 準備測試環(huán)境

3.1 新建 Stock Api

3.2 添加 Cors 過濾器

打開 StockController,修改代碼

<?php
namespace api\modules\v1\controllers;

use yii\filters\Cors;
use yii\helpers\ArrayHelper;

 
use yii\rest\ActiveController;

class  StockController  extends ActiveController
{
    public $modelClass = 'api\models\Stock';
  
    public function behaviors()
    {
          return ArrayHelper::merge([
                [
                        'class' => Cors::className(),
                        'cors' => [
                            'Origin' => ['http://test.local'],
                            'Access-Control-Request-Method' => [],
                            'Access-Control-Request-Headers'=>['*']
                        ],

                ],
        ], parent::behaviors());
    }

3.3 新建測試站點 test.local

添加數(shù)據(jù)文件 data.json

{
    "name": "ahcj"
}

添加文件 index.html

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
        
<title>CORS TEST</title>
</head>

<body id="body">
Please look at the Console and Network pannel ......
</body>

<script>
    
function getSameOrigin(){
        
        var data = null;

        var xhr = new XMLHttpRequest();

        xhr.addEventListener("readystatechange", function () {
            if (this.readyState === 4) {
                  
                console.log(this.responseText);
                
            }
        });

        xhr.open("GET", "data.json");

        xhr.send(data);
    
}   
    
function get(){
        
        var data = null;

        var xhr = new XMLHttpRequest();

        xhr.addEventListener("readystatechange", function () {
            if (this.readyState === 4) {
                  
                console.log(this.responseText);
                
            }
        });

        xhr.open("GET", "http://api.baojia.local/v1/stocks/3001");

        xhr.send(data);
    
}   


function post(){
    var data = "symbol=999999&code=sh999999&name=This%20is%20a%20test&ipo_date=2017-070-7";

    var xhr = new XMLHttpRequest();
 

    xhr.addEventListener("readystatechange", function () {
        if (this.readyState === 4) {
                console.log(this.responseText);
        }
    });

    xhr.open("POST", "http://api.baojia.local/v1/stocks");
    xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");

    xhr.send(data);
}

function put(){
        var data = "symbol=999999&code=sh999999&name=This%20is%20an%20update%20test&ipo_date=2017-070-7";

        var xhr = new XMLHttpRequest();
      
        xhr.addEventListener("readystatechange", function () {
          if (this.readyState === 4) {
            console.log(this.responseText);
          }
        });

        xhr.open("PUT", "http://api.baojia.local/v1/stocks/3001");
        xhr.setRequestHeader("content-type", "application/x-www-form-urlencoded");
        xhr.setRequestHeader("X-Custom-Header", "my-custom-header");

        xhr.send(data);
}

var x = 0;

if( x === 0 ){
       getSameOrigin()
}else if( x === 1){
     get();
}else if( x === 2){
    post();
}
else if( x === 3){
    put();
}

  

</script>

</html>


4 簡單請求

對于簡單請求,瀏覽器直接發(fā)出CORS請求。具體來說,就是在頭信息之中,增加一個Origin字段。

4.1 同源請求

修改 index.html,設置 x = 0 ,刷新瀏覽器

2017-08-24 11-55-15 的屏幕截圖.png

4.2 請求被拒絕

刷新瀏覽器,執(zhí)行 GET 請求,發(fā)現(xiàn)請求頭被自動添加了 Origin 字段。
Origin字段用來說明,本次請求來自哪個源(協(xié)議'+ 域名 + 端口)。服務器根據(jù)這個值,決定是否同意這次請求。

2017-08-23 17-32-39 的屏幕截圖.png
2017-08-23 17-19-36 的屏幕截圖.png
2017-08-23 17-20-57 的屏幕截圖.png

對比同源請求,CORS請求多了一個 Origin 請求頭,其他沒有多大區(qū)別。Origin包含協(xié)議名、地址以及一個可選的端口,她是瀏覽器自動添加的,無法手動添加。

請求被拒絕的情況下,CORS 響應頭和普通請求響應頭沒有區(qū)別。

4.3 請求被允許

修改 StockController,添加允許的請求方法

......

'Access-Control-Request-Method' => ['GET'],

......

刷新瀏覽器,重新訪問 GET 請求

2017-08-23 17-25-17 的屏幕截圖.png
2017-08-23 17-23-56 的屏幕截圖.png
2017-08-23 17-25-30 的屏幕截圖.png

請求成功,響應頭中會包含一些 以“Access-Control-”作為前綴的項目。

可能的響應字段有:

  1. Access-Control-Allow-Origin(必須)
    該字段是必須的。它的值要么是請求時Origin字段的值,要么是一個*,表示接受任意域名的請求。

  2. Access-Control-Allow-Credentials(可選)
    它的值是一個布爾值,表示是否允許發(fā)送Cookie。默認情況下,Cookie不包括在CORS請求之中。設為true,即表示服務器明確許可,Cookie可以包含在請求中,一起發(fā)給服務器。這個值也只能設為true,如果服務器不要瀏覽器發(fā)送Cookie,刪除該字段即可。

  3. Access-Control-Expose-Headers(可選)
    CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:
    Cache-Control | Content-Language | Content-Type | Expires | Last-Modified | Pragma
    如果想拿到其他字段,就必須在Access-Control-Expose-Headers里面指定,并以逗號進行分隔。

4.3 結(jié)論

4.3.1 不在許可范圍內(nèi)

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

4.3.2 在許可范圍內(nèi)

如果Origin指定的域名在許可范圍內(nèi),服務器返回的響應,會多出幾個頭信息字段。

5 非簡單請求

5.1 預檢請求

非簡單請求是那種對服務器有特殊要求的請求,比如請求方法是PUT或DELETE,或者Content-Type字段的類型是application/json。

非簡單請求的CORS請求,會在正式通信之前,增加一次HTTP查詢請求,稱為"預檢"請求(preflight)。

瀏覽器先詢問服務器,當前網(wǎng)頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發(fā)出正式的XMLHttpRequest請求,否則就報錯。

設置 js 變量 x = 3,刷新瀏覽器

2017-08-23 17-52-57 的屏幕截圖.png
2017-08-23 17-52-36 的屏幕截圖.png
2017-08-23 17-53-13 的屏幕截圖.png

可以看到只發(fā)送了一個 OPTIONS 請求,沒有發(fā)起 PUT 請求,原因是響應頭 Access-Control-Allow-Methods 沒有包含 PUT 方法。

預檢請求以 OPTIONS 方法發(fā)送,請求中包含:

  1. Origin: 表示請求來自哪個源。

  2. Access-Control-Request-Method – 該項內(nèi)容是實際請求的種類,可以是GET、POST之類的簡單請求,也可以是PUT、DELETE等等。

  3. Access-Control-Request-Headers – 該項是一個以逗號分隔的列表,當中是復雜請求所使用的頭部。


預檢請求可能的響應頭:

  1. Access-Control-Allow-Origin(必含)
    和簡單請求一樣的,必須包含一個域。

  2. Access-Control-Allow-Methods(必含)
    這是對預請求當中Access-Control-Request-Method的回復,這一回復將是一個以逗號分隔的列表。盡管客戶端或許只請求某一方法,但服務端仍然可以返回所有允許的方法,以便客戶端將其緩存。

  3. Access-Control-Allow-Headers(當預請求中包含Access-Control-Request-Headers時必須包含)
    這是對預請求當中Access-Control-Request-Headers的回復,和上面一樣是以逗號分隔的列表,可以返回所有支持的頭部。

  4. Access-Control-Allow-Credentials(可選)
    和簡單請求當中作用相同。

  5. Access-Control-Max-Age(可選)
    以秒為單位的緩存時間。預請求的的發(fā)送并非免費午餐,允許時應當盡可能緩存。

5.2 預檢請求的響應

5.2.1 修改 StockController,在響應頭 Access-Control-Allow-Methods: 包含 PUT 方法


'Access-Control-Request-Method' => ['GET','PUT'],

5.2.2 刷新瀏覽器,重新發(fā)起 PUT 請求

可以看到瀏覽器先執(zhí)行 OPTIONS 請求,然后再執(zhí)行 PUT 請求,兩個請求都成功了,原因是 OPTIONS 請求的響應頭 Access-Control-Allow-Methods 中包含 PUT 方法。

2017-08-23 18-05-29 的屏幕截圖.png
2017-08-23 18-05-15 的屏幕截圖.png
2017-08-23 18-06-49 的屏幕截圖.png

5.3 瀏覽器的正常請求和響應

一旦服務器通過了"預檢"請求,以后每次瀏覽器正常的CORS請求,就都跟簡單請求一樣,會有一個Origin頭信息字段。服務器的回應,也都會有一個Access-Control-Allow-Origin頭信息字段。

PUT 請求

2017-08-23 18-07-04 的屏幕截圖.png
2017-08-23 18-07-12 的屏幕截圖.png

6 參考

跨域資源共享 CORS 詳解
HTTP access control (CORS)
CORS——跨域請求那些事兒
利用CORS實現(xiàn)跨域請求

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容