你所不知道的跨域資源共享(CORS)

devsai發表於2019-03-03

本文同步更新於www.devsai.com

寫在前面

有沒有一看到講跨域資源共享的就不想再看的了,網上的跨域資源共享的博文,三天兩頭的就出一篇。

既然你已經進來看了,還請你稍稍忍耐下,繼續往下看,或許你會發現和之前看到的有不一樣的收穫。

其實,之前看過我寫的文章的同學可能知道,我寫過一篇關於《跨域及跨域資源共享》(沒有看過的同學,可以從這進去)。比較全面的介紹了跨域的多種解決方案,以及說明了跨域資源共享.

你們會不會想:那既然已經寫過了,為什麼又寫一篇? 是不是博主已經沒啥東西可寫了。

別急,接下來,讓我跟你們慢慢道來。

你們所知道的

看過之前寫的《跨域及跨域資源共享》或看過多篇CORS文章的同學可以選擇性的跳過這一小段了。

就像你們看到過的相關的文章,講跨域資源共享,一般講其原理時,必定要講到跨域資源共享的請求有兩種(也有很多沒有講到):

簡單請求 (Simple Request)
預檢測請求 (Preflight Request)

然後就會進一步的講到,什麼時候發只發簡單請求,又什麼時候會在發真實的請求前,先發預檢測請求,普遍的都是這麼說的(包括我之前寫的也是)

以下幾種情況時都滿足時是簡單請求

request header 是簡單的請求頭

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
等等非自定義的請求頭

request method 是下面的請求型別

HEAD
GET
POST

Content-Type 只限三個值

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

如果不滿足以上條件的都會先傳送預檢測請求,即為OPTIONS請求型別的請求

幾乎都是這麼說的,差別只是描述方式不同,比例下面的別人寫的:

你所不知道的跨域資源共享(CORS)

什麼? 不信, 那你隨便搜尋幾篇相關的文章看看。

那麼,這有什麼問題

先來做個例子吧。

假設要實現帶進度條的上傳功能,介面不是同域上的,服務端已經給配置了支援跨域資源共享的響應頭,那我們直接用XmlHttpRequest就可以了

javaScript程式碼大概如下:

var xhr = new XmlHttpRequest();
xhr.onreadystatechange = function() {
    // do something
}

xhr.upload.onprogress = function(){
    // do something
}

xhr.open('POST','http://127.0.0.1/upload');
var fd = new FormData();
fd.append('file',file);
xhr.send(fd);複製程式碼

然後上傳檔案並檢視下請求

你所不知道的跨域資源共享(CORS)

What? 為什麼會有兩個請求啊。 是不是它不滿足簡單請求的要求(已不記得簡單請求的同學往上再看看)

那麼,我們來看看該真實請求的請求頭

你所不知道的跨域資源共享(CORS)

簡單請求要求:

要求點 實際內容 是否滿足
請求方式 POST 滿足
請求頭 都是非自定義的請求頭 滿足
Content-Type multipart/form-data 滿足

不是都滿足了嗎?那為什麼,會有兩個請求,為什麼在傳送真實請求前還發了OPTIONS方式的請求。

為什麼!!!整個人感覺都不好了!!!

變個魔術

把上面的javaScript程式碼改動下:

var xhr = new XmlHttpRequest();
xhr.onreadystatechange = function() {
    // do something
}

xhr.open('POST','http://127.0.0.1/upload');
var fd = new FormData();
fd.append('file',file);
xhr.send(fd);複製程式碼

去掉了xhr.upload.onprogress,上傳後再來看下請求及請求頭:

你所不知道的跨域資源共享(CORS)

你所不知道的跨域資源共享(CORS)

只有一個請求,請求頭內容還都一樣。 (這到底是怎麼回事...有種再也不相信愛情的趕覺了!!!)

看到這裡的同學,有木有覺得博主在坑你們,放了兩張相同的請求頭截圖就想糊弄。

俗話說得好,不試不知道,一試嚇一跳,要不,你們也親自試試,一試便知真假。

再次雙手奉上demo(喜歡的順便點個贊哦~)。

分析問題

從上面的兩小段JS看出,只是去除了上傳的進度資訊事件。也就是說加了進度事件就多發了個預檢測請求。

那麼,還有沒有其他的事件了?新增其他的事件會不會也會傳送預檢測請求呢?

事件有onerror,onloadstart等等。經過博主的測試,上述答案是肯定的,新增其他事件後,確實也會發生預檢測請求。

追求真理的同學們,在博主的demo裡改改試試吧。

現在已經知道了問題的所在,由上傳相關事件導致了跨域請求多發了預檢測請求,說好的簡單請求(Simple Request)呢~

博主抱著對問題刨根問底的精神,再次檢視cors相關文件,找到了如下的內容:

  • If the following conditions are true, follow the simple cross-origin request algorithm:

    • The request method is a simple method and the force preflight flag is unset.

    • Each of the author request headers is a simple header or author request headers is empty.

通過這段,我們知道,原來除了我們所知道的簡單請求的幾大特徵外,還提到了force preflight flag,這是什麼鬼?

難道是因為設定了它? 那麼什麼時候設定了force preflight flag?

上面我們知道了因為上傳事件導致了傳送預檢測請求,會不會是上傳監聽事件的時候給設定了force preflight flag
然後在XmlHttpRequest level 2 中的找到了相關的內容,以證實我的猜測是正確的。

有下面幾段內容:

force preflight flag
The upload events flag.

從這段可以知道,force preflight flagupload events flag是對應的,看到這裡就知道了,只要upload events flag被設定true

那麼就等於force preflight flag被設定了true,這時,不管請求的型別的是不是simple method,也不管請求頭是不是simple header,都會先傳送預檢測請求。

接下來,我們再來看看upload events flag會在什麼情況下被設定呢?

If the asynchronous flag is true and one or more event listeners are registered on the XMLHttpRequestUpload object set the upload events flag to true. Otherwise, set the upload events flag to false.

原來,當asynchronous flagtrue並且XMLHttpRequestUpload(即示例中的xhr.upload)的一個或多個事件被監聽的時候,upload listener flag就會被設定了。

這也正如之前測試的,當加了xhr.upload.onprogress後,出現了預檢測請求。

到這裡總算水落石出了。

這裡還需要說明的一點是,the asynchronous flag就是xhr.open()的第三個引數,當未設定第三引數時,預設為非同步,也就是the asynchronous flagtrue

如果第三個引數設定為false,那麼即使有上傳的監聽事件也不會傳送預檢測請求(Preflight Reuqest)

總結

以後還會不會理直氣壯的在別人面前說,只要是滿足幾大條件(是非自定義的請求頭,是GETorPOSTorHEAD,或Content-Type是那三種值的)就是簡單請求 ,就不會發生預檢測請求。

通過本文可知,並非滿足這幾大條件就一定是簡單請求的,
應該要加個前置條件,是否是在上傳請求中跨域,是否是非同步的,是否監聽了上傳事件。

看到這,可能你想說,寫這麼多有啥用,對實際開發有幫助嗎?或許沒什麼實際的幫助吧,又或許你也不會碰到吧。

但,最起碼當你碰到的時候,你看到了兩個請求,再看了下程式碼,你已經心裡就有數了,知道這是怎麼一回事了。

一直認為,做技術的對碰到的問題要知其然,更要知其所以然。

相關文章