關於CORS 應該注意的幾點

瀟湘待雨發表於2019-07-01

前言

對於跨域,隨著w3c的CORS的出現,相比較於有些年頭的jsonp,CORS以其簡單安全,支援post的優勢越來越收到大家的歡迎。具體如何CORS的原理和實現,直接推薦阮老師的文章,十分詳細。本文主要關注CORS實現過程中的幾個疑惑點。

預檢請求

背景

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

簡單請求

同時滿足一下條件的即是簡單請求:

  1. 請求方法是以下三種方法之一:
    HEAD、GET、POST
  2. HTTP的頭資訊不超出以下幾種欄位
    Accept
    Accept-Language
    Content-Language
    Last-Event-ID
    Content-Type:只限於三個值application/x-www-form、multipart/form-data、text/plain

    非簡單請求

    顯然,不同時滿足則為非簡單請求(可以認為是複雜請求)。兩者的差別在於複雜請求在與服務端互動時多了一次options的預檢請求,畢竟複雜請求一般就是HTTP請求頭資訊超出限制或者method為put、delete等操作行為,處於安全考慮,需要服務端先行驗證來決定是否給予相關許可權。

如下所示(示例來自阮老師文章):

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
// PUT method為複雜請求,要預檢
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

非簡單請求,瀏覽器自動傳送otpios的預檢請求,請求頭如下:

OPTIONS /cors HTTP/1.1
// 請求源
Origin: http://api.bob.com
// 必須欄位,指明正式cors請求將會使用那些method
Access-Control-Request-Method: PUT
// 除簡單頭之外,額外的請求頭
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

對於預檢資訊,服務端一般做了如下操作:

1、檢查origin、Access-Control-Request-Method和Access-Control-Request-Headers等欄位,確認是否允許跨域,如果允許跨域作出迴應:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
// 允許的源 
Access-Control-Allow-Origin: http://api.bob.com
// 允許的請求方式
Access-Control-Allow-Methods: GET, POST, PUT
// 允許額外header
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

如果不允許跨域,依然響應該請求,不過不攜帶CORS相關的資訊。瀏覽器則會認為伺服器不允許跨域,觸發錯誤。

// 常見的跨域錯誤
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

到這裡一個流程結束,不過我們要關注的是options 預檢請求之後 code 返回的問題

options 成功之後,返回code 200 還是 204

常規預檢的就是對於options的請求直接返回code 200的響應,表示校驗通過。
但是前兩天發現有的返回為code204。兩者之間的差別具體在哪呢。

常見用法

1、針對特定介面支援CORS時,在程式碼里加判斷對於options返回200

// 隨便找了段java程式碼 
if (req.getMethod().equals("OPTIONS")) {
     res.setStatus(200);
 }

2、如果整個域名都支援CORS,可以再nginx側直接配置,此時常見的是返回204.

if ($request_method = 'OPTIONS') { 
    add_header Access-Control-Allow-Origin *; 
    add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
    #****省略...
    return 204; 
}

總結

兩者之間的差別,首先可以參考下204和200 對應的含義(下面內容摘自MDN)。
200
請求成功,成功的具體含義依據http method 的不同而有所差別。:

  • GET: 資源已經被提取並在訊息中文中傳遞
  • POST: 描述動作結果的資源在訊息體中傳輸

204
伺服器成功處理了請求,但不需要返回任何實體內容,並且希望返回更新了的元資訊。
客戶端是瀏覽器的haul,使用者瀏覽器應保留髮送了該請求的頁面,而不產生任何文件檢視上的變化。由於204響應被禁止包含任何訊息體,因此它始終以訊息頭後的第一個空行結尾。

簡單總結,204返回表示請求成功,並且無訊息體,優勢在於節省網路請求。

具體到options請求,選用哪一個。

貼切的來說,應該像其他options請求一樣為預檢optiosn請求返回相同的code狀態碼,相關規範不要求或者推薦其他內容。
fecth請求
例如對於Fetch 規範 要求CORS協議的status可以為200-209裡面的任意值。

If a CORS check for request and response returns success 
and response’s status is an ok status, 
run these substeps.

如果response為一個okstatus就可以繼續執行

An ok status is any status in the range 200 to 299, inclusive.

並不要求具體哪一個值。
所以從fetch來看,兩者均可選擇。

HTTP 1.1
對於http/1.1 規範來說,有一章節專門定義了各種響應code。對於2開頭的2-XXcode,分別描述如下:

  • 200
    請求成功,成功的具體含義依據http method 的不同而有所差別。
  • GET: 資源已經被提取並在訊息中文中傳遞
  • POST: 描述動作結果的資源在訊息體中傳輸
  • OPTIONS: communications options成功的表示
    由上可知,對於options預檢請求的響應,需要包含下面兩種情況:
    1、表明請求成功
    2、描述通訊選項(這裡包括, Access-Control-Allow-Methods 和 Access-Control-Allow-Headers這些響應頭)
    看起來,上面就是200在http定義中的含義,顯然滿足,但是如果繼續看204的含義,好像也可以滿足需求。

204
伺服器成功處理了請求,但不需要返回任何實體內容,並且希望返回更新了的元資訊。
客戶端是瀏覽器的話,使用者瀏覽器應保留髮送了該請求的頁面,而不產生任何文件檢視上的變化。由於204響應被禁止包含任何訊息體,因此它始終以訊息頭後的第一個空行結尾。

結論

首先兩者都可以使用,對於200,從定義而言更符合場景和定義。但是204無訊息體,優勢在於節省網路請求。
至於用哪個,大家自行做下判斷。

跨域 讀取cookie

作為常見的場景,cookie一般會存放一些,鑑權會話等資訊。對於CORS跨域,預設的是不包含cookie的。

A cross-origin request by default does not bring any credentials (cookies or HTTP authentication)

如果要操作cookie需要分別從服務端和客戶端兩個場景來看。

客戶端 request 攜帶cookie

request如果要攜帶cookie,需要特定引數指明。可能看到過這個引數為credentials或者withCredentials,什麼時候用兩者呢。主要跟請求的實現有關:

  1. Fetch 使用credentials
    直接使用原生Fetch的話,需要設定credentials。

    credentials 是Request介面的只讀屬性,用於表示使用者代理是否應該在跨域請求的情況下從其他域傳送cookies。這與XHR的withCredentials 標誌相似,不同的是有三個可選值(後者是兩個):

  • omit: 從不傳送cookies.
  • same-origin: 只有當URL與響應指令碼同源才傳送 cookies、 HTTP Basic authentication 等驗證資訊.(瀏覽器預設值,在舊版本瀏覽器,例如safari 11依舊是omit,safari 12已更改)
  • include: 不論是不是跨域的請求,總是傳送請求資源域在本地的 cookies、 HTTP Basic authentication 等驗證資訊.

CORS跨域的時候,只需要如下設定:

fetch('http://another.com', {
  credentials: "include"
});
  1. XHR 使用withCredentials
    基於XMLHttpRequest實現的請求使用withCredentials來允許攜帶cookie。
    該屬性為boolean型別,所以只有true/false兩個取值,預設為false。
    這樣也很好理解,預設不攜帶是處於安全考慮。
    使用如下
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true;
xhr.send(null);

適用框架:jquery的ajax,axios等。

服務端 Access-Control-Allow-Credentials

當客戶端設定了允許攜帶cookie之後,並不能完成該操作,畢竟是跨域,服務端也需要做響應設定,否則瀏覽器拿不到正確響應。

Access-Control-Allow-Credentials:true

看MDN 的解釋:


The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend JavaScript code when the request's credentials mode (Request.credentials) is "include".  

當 credentials為include的時候,通知瀏覽器是否將響應暴露給前端jscode,如果為false,js不能讀取響應自然請求報錯。
只有Access-Control-Allow-Credentials為true時,才會將響應暴露給客戶端。
當作為預檢請求響應頭時,表明該實際請求(即後面的真正請求)是否可以使用credentials。

不過對於簡單請求,因為沒有預檢,如果服務端沒有正確響應,瀏覽器會忽略該屬性,並不會直接報錯。
需要與XMLHttpRequest.withCredentials屬性或者Fetch 的credentials 配合使用。

注意

如果要傳送Cookie,Access-Control-Allow-Origin就不能設為星號,必須指定明確的、與請求網頁一致的域名。
同時,Cookie依然遵循同源政策,只有用伺服器域名設定的Cookie才會上傳,其他域名的Cookie並不會上傳。
且(跨源)原網頁程式碼中的document.cookie也無法讀取伺服器域名下的Cookie。

畢竟cookie是有path來保證封閉性的,如果可以隨便讀取不管從安全還是效能上都是一種隱患。

多域名跨域

對於多域名跨域,方法比較多。

1、Access-Control-Allow-Origin:*
允許任意域名跨域,顯然支援多域名。不過從安全性和cookie的使用的角度來看並不推薦。

2、動態匹配域名

這種實現方式比較多,原理就是宣告允許的多域名配置,可以是陣列或者是正則,根據當前請求的域名,來判斷是否在適用返回內,在的話則設定Access-Control-Allow-Origin為當前域名。

具體實現這裡就不寫了。

結束語

參考文章

http://www.ruanyifeng.com/blog/2016/04/cors.html
https://fetch.spec.whatwg.org/#cors-protocol-and-credentials
http://www.yunweipai.com/archives/9381.html
以上是在工作中偶然發現的幾點疑惑,解決之後深究了下具體原理。希望能對其他同學有所幫助,拋磚引玉,一起努力。

相關文章