CORS原理詳解

JSONP並不是一個好的跨域解決方案,它至少有着下面兩個嚴重問題:

  1. 會打亂服務器的消息格式:JSONP要求服務器響應一段JS代碼,但在非跨域的情況下,服務器又需要響應一個正常的JSON格式
  2. 只能完成GET請求:JSONP的原理會要求瀏覽器端生成一個script元素,而script元素髮出的請求只能是get請求

所以,CORS是一種更好的跨域解決方案。

概述

CORS是基於http1.1的一種跨域解決方案,它的全稱是Cross-Origin Resource Sharing,跨域資源共享。

它的總體思路是:如果瀏覽器要跨域訪問服務器的資源,需要獲得服務器的允許

在這裏插入圖片描述

而要知道,一個請求可以附帶很多信息,從而會對服務器造成不同程度的影響

比如有的請求只是獲取一些新聞,有的請求會改動服務器的數據

針對不同的請求,CORS規定了三種不同的交互模式,分別是:

  • 簡單請求
  • 需要預檢的請求
  • 附帶身份憑證的請求

這三種模式從上到下層層遞進,請求可以做的事越來越多,要求也越來越嚴格。

下面分別說明三種請求模式的具體規範。

簡單請求

當瀏覽器端運行了一段ajax代碼(無論是使用XMLHttpRequest還是fetch api),瀏覽器會首先判斷它屬於哪一種請求模式

簡單請求的判定

當請求同時滿足以下條件時,瀏覽器會認爲它是一個簡單請求:

  1. 請求方法屬於下面的一種:

    • get
    • post
    • head
  2. 請求頭僅包含安全的字段,常見的安全字段如下:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  3. 請求頭如果包含Content-Type,僅限下面的值之一:

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

如果以上三個條件同時滿足,瀏覽器判定爲簡單請求。

下面是一些例子:
地址是虛假的

// 簡單請求
fetch("http://crossdomain.com/api/news");

// 請求方法不滿足要求,不是簡單請求
fetch("http://crossdomain.com/api/news", {
  method:"PUT"
})

// 加入了額外的請求頭,不是簡單請求
fetch("http://crossdomain.com/api/news", {
  headers:{
    a: 1
  }
})

// 簡單請求
fetch("http://crossdomain.com/api/news", {
  method: "post"
})

// content-type不滿足要求,不是簡單請求
fetch("http://crossdomain.com/api/news", {
  method: "post",
  headers: {
    "content-type": "application/json"
  }
})

簡單請求的交互規範

當瀏覽器判定某個ajax跨域請求簡單請求時,會發生以下的事情

  1. 請求頭中會自動添加Origin字段

比如,在頁面http://my.com/index.html中有以下代碼造成了跨域

// 簡單請求
fetch("http://crossdomain.com/api/news");

請求發出後,請求頭會是下面的格式:

GET /api/news/ HTTP/1.1
Host: crossdomain.com
Connection: keep-alive
...
Referer: http://my.com/index.html
Origin: http://my.com

看到最後一行沒,Origin字段會告訴服務器,是哪個源地址在跨域請求

  1. 服務器響應頭中應包含Access-Control-Allow-Origin

當服務器收到請求後,如果允許該請求跨域訪問,需要在響應頭中添加Access-Control-Allow-Origin字段

該字段的值可以是:

  • *:表示我很開放,什麼人我都允許訪問
  • 具體的源:比如http://my.com,表示我就允許你訪問

實際上,這兩個值對於客戶端http://my.com而言,都一樣,因爲客戶端纔不會管其他源服務器允不允許,就關心自己是否被允許

當然,服務器也可以維護一個可被允許的源列表,如果請求的Origin命中該列表,才響應*或具體的源

爲了避免後續的麻煩,強烈推薦響應具體的源

假設服務器做出了以下的響應:

HTTP/1.1 200 OK
Date: Tue, 21 Apr 2020 08:03:35 GMT
...
Access-Control-Allow-Origin: http://my.com
...

消息體中的數據

當瀏覽器看到服務器允許自己訪問後,高興的像一個兩百斤的孩子,於是,它就把響應順利的交給js,以完成後續的操作

下圖簡述了整個交互過程

在這裏插入圖片描述

需要預檢的請求

簡單的請求對服務器的威脅不大,所以允許使用上述的簡單交互即可完成。

但是,如果瀏覽器不認爲這是一種簡單請求,就會按照下面的流程進行:

  1. 瀏覽器發送預檢請求,詢問服務器是否允許
  2. 服務器允許
  3. 瀏覽器發送真實請求
  4. 服務器完成真實的響應

比如,在頁面http://my.com/index.html中有以下代碼造成了跨域

// 需要預檢的請求
fetch("http://crossdomain.com/api/user", {
  method:"POST", // post 請求
  headers:{  // 設置請求頭
    a: 1,
    b: 2,
    "content-type": "application/json"
  },
  body: JSON.stringify({ name: "袁小進", age: 18 }) // 設置請求體
});

瀏覽器發現它不是一個簡單請求,則會按照下面的流程與服務器交互

  1. 瀏覽器發送預檢請求,詢問服務器是否允許
OPTIONS /api/user HTTP/1.1
Host: crossdomain.com
...
Origin: http://my.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: a, b, content-type

可以看出,這並非我們想要發出的真實請求,請求中不包含我們的響應頭,也沒有消息體。

這是一個預檢請求,它的目的是詢問服務器,是否允許後續的真實請求。

預檢請求沒有請求體,它包含了後續真實請求要做的事情

預檢請求有以下特徵:

  • 請求方法爲OPTIONS
  • 沒有請求體
  • 請求頭中包含
    • Origin:請求的源,和簡單請求的含義一致
    • Access-Control-Request-Method:後續的真實請求將使用的請求方法
    • Access-Control-Request-Headers:後續的真實請求會改動的請求頭
  1. 服務器允許

服務器收到預檢請求後,可以檢查預檢請求中包含的信息,如果允許這樣的請求,需要響應下面的消息格式

HTTP/1.1 200 OK
Date: Tue, 21 Apr 2020 08:03:35 GMT
...
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: a, b, content-type
Access-Control-Max-Age: 86400
...

對於預檢請求,不需要響應任何的消息體,只需要在響應頭中添加:

  • Access-Control-Allow-Origin:和簡單請求一樣,表示允許的源
  • Access-Control-Allow-Methods:表示允許的後續真實的請求方法
  • Access-Control-Allow-Headers:表示允許改動的請求頭
  • Access-Control-Max-Age:告訴瀏覽器,多少秒內,對於同樣的請求源、方法、頭,都不需要再發送預檢請求了
  1. 瀏覽器發送真實請求

預檢被服務器允許後,瀏覽器就會發送真實請求了,上面的代碼會發生下面的請求數據

POST /api/user HTTP/1.1
Host: crossdomain.com
Connection: keep-alive
...
Referer: http://my.com/index.html
Origin: http://my.com

{"name": "鴻宇哥哥", "age": 18 }
  1. 服務器響應真實請求
HTTP/1.1 200 OK
Date: Tue, 21 Apr 2020 08:03:35 GMT
...
Access-Control-Allow-Origin: http://my.com
...

添加用戶成功

可以看出,當完成預檢之後,後續的處理與簡單請求相同

下圖簡述了整個交互過程

image-20200421165913320

附帶身份憑證的請求

默認情況下,ajax的跨域請求並不會附帶cookie,這樣一來,某些需要權限的操作就無法進行

不過可以通過簡單的配置就可以實現附帶cookie

// xhr
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// fetch api
fetch(url, {
  credentials: "include"
})

這樣一來,該跨域的ajax請求就是一個附帶身份憑證的請求

當一個請求需要附帶cookie時,無論它是簡單請求,還是預檢請求,都會在請求頭中添加cookie字段

而服務器響應時,需要明確告知客戶端:服務器允許這樣的憑據

告知的方式也非常的簡單,只需要在響應頭中添加:Access-Control-Allow-Credentials: true即可

對於一個附帶身份憑證的請求,若服務器沒有明確告知,瀏覽器仍然視爲跨域被拒絕。

另外要特別注意的是:對於附帶身份憑證的請求,服務器不得設置 Access-Control-Allow-Origin 的值爲*。這就是爲什麼不推薦使用*的原因

一個額外的補充

在跨域訪問時,JS只能拿到一些最基本的響應頭,如:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要訪問其他頭,則需要服務器設置本響應頭。

Access-Control-Expose-Headers頭讓服務器把允許瀏覽器訪問的頭放入白名單,例如:

Access-Control-Expose-Headers: authorization, a, b

這樣JS就能夠訪問指定的響應頭了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章