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

標(biāo)簽(空格分隔): Yii2


1 什么是CORS

CORS是一個(gè)W3C標(biāo)準(zhǔn),全稱是"跨域資源共享"(Cross-origin resource sharing)。

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

2 CORS 請(qǐng)求分類

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

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

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

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

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

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

3 準(zhǔn)備測(cè)試環(huán)境

3.1 新建 Stock Api

3.2 添加 Cors 過(guò)濾器

打開(kāi) 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 新建測(cè)試站點(diǎn) 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 簡(jiǎn)單請(qǐng)求

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

4.1 同源請(qǐng)求

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

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

4.2 請(qǐng)求被拒絕

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

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

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

請(qǐng)求被拒絕的情況下,CORS 響應(yīng)頭和普通請(qǐng)求響應(yīng)頭沒(méi)有區(qū)別。

4.3 請(qǐng)求被允許

修改 StockController,添加允許的請(qǐng)求方法

......

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

......

刷新瀏覽器,重新訪問(wèn) GET 請(qǐng)求

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

請(qǐng)求成功,響應(yīng)頭中會(huì)包含一些 以“Access-Control-”作為前綴的項(xiàng)目。

可能的響應(yīng)字段有:

  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里面指定,并以逗號(hào)進(jìn)行分隔。

4.3 結(jié)論

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

如果 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。

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

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

5 非簡(jiǎn)單請(qǐng)求

5.1 預(yù)檢請(qǐng)求

非簡(jiǎn)單請(qǐng)求是那種對(duì)服務(wù)器有特殊要求的請(qǐng)求,比如請(qǐng)求方法是PUT或DELETE,或者Content-Type字段的類型是application/json。

非簡(jiǎn)單請(qǐng)求的CORS請(qǐng)求,會(huì)在正式通信之前,增加一次HTTP查詢請(qǐng)求,稱為"預(yù)檢"請(qǐng)求(preflight)。

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

設(shè)置 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ā)送了一個(gè) OPTIONS 請(qǐng)求,沒(méi)有發(fā)起 PUT 請(qǐng)求,原因是響應(yīng)頭 Access-Control-Allow-Methods 沒(méi)有包含 PUT 方法。

預(yù)檢請(qǐng)求以 OPTIONS 方法發(fā)送,請(qǐng)求中包含:

  1. Origin: 表示請(qǐng)求來(lái)自哪個(gè)源。

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

  3. Access-Control-Request-Headers – 該項(xiàng)是一個(gè)以逗號(hào)分隔的列表,當(dāng)中是復(fù)雜請(qǐng)求所使用的頭部。


預(yù)檢請(qǐng)求可能的響應(yīng)頭:

  1. Access-Control-Allow-Origin(必含)
    和簡(jiǎn)單請(qǐng)求一樣的,必須包含一個(gè)域。

  2. Access-Control-Allow-Methods(必含)
    這是對(duì)預(yù)請(qǐng)求當(dāng)中Access-Control-Request-Method的回復(fù),這一回復(fù)將是一個(gè)以逗號(hào)分隔的列表。盡管客戶端或許只請(qǐng)求某一方法,但服務(wù)端仍然可以返回所有允許的方法,以便客戶端將其緩存。

  3. Access-Control-Allow-Headers(當(dāng)預(yù)請(qǐng)求中包含Access-Control-Request-Headers時(shí)必須包含)
    這是對(duì)預(yù)請(qǐng)求當(dāng)中Access-Control-Request-Headers的回復(fù),和上面一樣是以逗號(hào)分隔的列表,可以返回所有支持的頭部。

  4. Access-Control-Allow-Credentials(可選)
    和簡(jiǎn)單請(qǐng)求當(dāng)中作用相同。

  5. Access-Control-Max-Age(可選)
    以秒為單位的緩存時(shí)間。預(yù)請(qǐng)求的的發(fā)送并非免費(fèi)午餐,允許時(shí)應(yīng)當(dāng)盡可能緩存。

5.2 預(yù)檢請(qǐng)求的響應(yīng)

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


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

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

可以看到瀏覽器先執(zhí)行 OPTIONS 請(qǐng)求,然后再執(zhí)行 PUT 請(qǐng)求,兩個(gè)請(qǐng)求都成功了,原因是 OPTIONS 請(qǐng)求的響應(yīng)頭 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 瀏覽器的正常請(qǐng)求和響應(yīng)

一旦服務(wù)器通過(guò)了"預(yù)檢"請(qǐng)求,以后每次瀏覽器正常的CORS請(qǐng)求,就都跟簡(jiǎn)單請(qǐng)求一樣,會(huì)有一個(gè)Origin頭信息字段。服務(wù)器的回應(yīng),也都會(huì)有一個(gè)Access-Control-Allow-Origin頭信息字段。

PUT 請(qǐng)求

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

6 參考

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

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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