前端工程師須知的CORS知識

屌絲原始碼發表於2019-04-20

背景

前端和後臺請求互動,若請求者(Client)和資源者響應者(Server)處在不同的域名,會發生跨域請求,不同域是指HTTP協議、域名、埠有一個或全都不相同。出於安全考慮瀏覽器限制了從指令碼內發起的資源跨域請求,CORS是解決跨域請求的一種機制。

一、什麼CORS

CORS全稱“跨域資源共享”,它是一種機制通過新增額外的HTTP頭部告訴瀏覽器,允許origin上的web應用訪問不同源伺服器資源,可以在XMLHttpRequest、Fetch中使用CORS發起跨域請求。

二、CORS工作原理

對於前端開發使用CORS發起跨域請求不需要額外工作,當請求發生時瀏覽器會自動設定相應的HTTP頭部資訊(下面介紹道),伺服器端需要做相應的頭部設定來控制跨域許可權。

1. 發起請求

對於使用CORS發起跨域請求可分為簡單請求和非簡請求兩種,簡單請求不會觸發OPTIONS預檢請求,可直接發起跨域請求,非簡單請求會觸發OPTIONS預檢請求,向伺服器發起一些詢問資訊,例如:是否允許這次跨域請求、告知用什麼請求方法、是否支援在HTTP頭裡攜帶自定義欄位。

1.1 簡單請求

簡單請求可直接向伺服器請求,只要伺服器同意這次跨域求,瀏覽器就能獲取到伺服器返回的資源,在請求中只要同時滿足以下條件就屬於簡單請求。

a. 使用GET、POST、HEAD 三者之一的請求方法。

b. 不得設定Accept 、 Accept-Language、 Content-Language,Content-Type之外的頭部資訊集合欄位,對於Content-Type的值不能設定text/plain, multipart/form-data, application/x-www-form-urlencode之外的 值。

c. 請求中的任意XMLHttpRequestupload物件均未設定任何監聽器。

d. 請求中沒有使用 ReadableStream 物件。

簡單例子

var request = new XMLHttpRequest()
var url = 'http://bar.other/resources/public-data/'

function callOtherDomain() {
  if(invocation) {    
    invocation.open('GET', url, true);
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}
//發起請求
callOtherDomain()
複製程式碼

請求發出時Client與Server間通過HTTP頭部欄位處理跨域許可權。

HTTP請求頭資訊
1. GET /resources/public-data/ HTTP/1.1
2. Host: bar.other
3. User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; 
4. rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
5. Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
6. Accept-Language: en-us,en;q=0.5
7. Accept-Encoding: gzip,deflate
8. Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
9. Connection: keep-alive
10. Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
11. Origin: http://foo.example
複製程式碼
伺服器響應頭部資訊
1. HTTP/1.1 200 OK
2. Date: Mon, 01 Dec 2008 00:23:53 GMT
3. Server: Apache/2.0.61 
4. Access-Control-Allow-Origin: *
5. Keep-Alive: timeout=2, max=100
6. Connection: Keep-Alive
7. Transfer-Encoding: chunked
8. Content-Type: application/xml
複製程式碼

Client通過Origin欄位告訴Server我是來自http://foo.example的請求,允許我訪問你的資源嗎?Server做出回應並在響應頭裡帶上Access-Control-Allow-Origin:* 表示我允許所有的外域訪問我的資源。

如果將Access-Control-Allow-Origin值設定成http://foo.example,表示Server上的資源只允許來自http://foo.example的源訪問。後臺開發一般會在此新增請求源白名單來控制允許那些請求源訪問服務資源。

如果Origin不是Access-Control-Allow-Origin允許的源,瀏覽器將攔截請求響應內容,無法獲取到伺服器資源。

1.2 非簡單請求

只要不滿足簡單請求的都屬於非簡單請求。非簡單請求會在正式發起資源訪問請求前觸發一個options 預檢請求,options請求不會對伺服器造成資源影響。 只要滿足下列條件之一就會觸發預檢請求:

a. PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH。

b. 在請求頭中設定Accept Accept-Language、 Content-Language、 Content-Type (需要注意額外的限制)、 DPR、 Downlink、 Save-Data、 Viewport-Width、 Width的欄位。

c. Content-Type 的值不屬於application/x-www-form-urlencoded、 multipart/form-data、 text/plain三者之一。

d. 請求中的XMLHttpRequestUpload 物件註冊了任意多個事件監聽器。

e. 請求中使用了ReadableStream物件。

再次借用MSDN中的例子說下

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '<?xml version="1.0"?><person><name>Arun</name></person>';
    
function callOtherDomain(){
  if(invocation)
    {
      invocation.open('POST', url, true);
      invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
      invocation.setRequestHeader('Content-Type', 'application/xml');
      invocation.onreadystatechange = handler;
      invocation.send(body); 
    }
}
// 發起請求
callOtherDomain()
複製程式碼

在程式碼中可以看到,我們人為的自定義了X-PINGOTHER請求頭欄位(滿足非簡單請求b條件),並且Content-Type被設定application/xml (滿足非簡單請求c條件),所以該請求首先會觸發一個options請求。

下面是options請求頭的部分資訊:

 1.OPTIONS /resources/post-here/ HTTP/1.1
 2.Host: bar.other
 3.User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
 4.Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
 5.Accept-Language: en-us,en;q=0.5
 6.Accept-Encoding: gzip,deflate
 7.Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
 8.Connection: keep-alive
 9.Origin: http://foo.example
10.Access-Control-Request-Method: POST
11.Access-Control-Request-Headers: X-PINGOTHER, Content-Type
複製程式碼

options 請求告訴Server, 我是來自http://foo.example的請求,在正式請求時,我會在請求頭中攜帶自定義欄位X-PINGOTHER、Conten-Type並且我將通過POST發起請求,你同意不?以下是用於向Server詢問的頭部資訊欄位:

Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
複製程式碼

我們看下Server對options請求是如何回應的

1.HTTP/1.1 200 OK
2.Date: Mon, 01 Dec 2008 01:15:39 GMT
2.Server: Apache/2.0.61 (Unix)
3.Access-Control-Allow-Origin: http://foo.example
4.Access-Control-Allow-Methods: POST, GET, OPTIONS
5.Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
6.Access-Control-Max-Age: 86400
7.Vary: Accept-Encoding, Origin
8.Content-Encoding: gzip
9.Content-Length: 0
10.Keep-Alive: timeout=2, max=100
11.Connection: Keep-Alive
12.Content-Type: text/plain
複製程式碼

從上面的options響應頭裡得知, 首部欄位 Access-Control-Allow-Origin表明伺服器允許http://foo.example請求源訪問資源。 欄位 Access-Control-Allow-Headers 表明伺服器允許請求頭中攜帶欄位 X-PINGOTHER 與 Content-Type。 Access-Control-Allow-Methods告知允許正式請求使用POST方法。 options請求結束,如果Server同意訪問,接下來就會發起正式的請求,否則結束請求。

第6行程式碼中有一段Access-Control-Max-Age: 86400資訊,這段資訊的意思是說在接下來的86400秒內對於同一請求不用再發起options預檢請求。Access-Control-Max-Age單位是秒,由後臺設定但不能超過瀏覽器支援的最大有效時間,否則不會生效。

2. 附帶身份憑證的請求

Fetch 與 CORS 可以基於 HTTP cookies 和 HTTP 認證資訊傳送身份憑證。一般而言,對於跨域 XMLHttpRequest 或 Fetch 請求,瀏覽器不會傳送身份憑證資訊。

如果要傳送憑證資訊,需要設定 XMLHttpRequest 的withCredentials=true,同時Server端也要設定響應的頭部欄位Access-Control-Allow-Credentials: true,才能在請求中攜帶身份憑證,否則,瀏覽器會攔截響應內容,不會把它返回給請求傳送者。

需要注意,Server如果設定了Access-Control-Allow-Origin:*同時請求又攜帶了cookies資訊,會導致請求失敗。

3. 跨域請求頭和響應頭欄位說明

3.1 請求頭欄位

Origin欄位表明預檢請求或實際請求的源站,origin 引數的值為源站 URI。它不包含任何路徑資訊,只是伺服器名稱,不管是否為跨域請求都會傳送Origin欄位。

Origin: <origin>
複製程式碼

Access-Control-Request-Method欄位用於預檢請求。其作用是,將實際請求所使用的 HTTP方法告訴伺服器。

Access-Control-Request-Method: <method>
複製程式碼

Access-Control-Request-Headers欄位用於預檢請求。其作用是,將實際請求所攜帶的首部欄位告訴伺服器。

Access-Control-Request-Headers: <field-name>[, <field-name>]*
複製程式碼

3.2 響應頭欄位

Access-Control-Allow-Origin欄位,origin 引數的值指定了允許訪問該資源的外域 URI。對於不需要攜帶身份憑證的請求,伺服器可以指定該欄位的值為萬用字元,表示允許來自所有域的請求。

Access-Control-Allow-Origin: <origin> | *
複製程式碼

Access-Control-Expose-Headers欄位,設定瀏覽器可以拿到的頭部欄位白名單。 在跨域訪問時,XMLHttpRequest物件的getResponseHeader()方法只能拿到一些最基本的響應頭,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要訪問其他頭,需要伺服器設定本響應頭,這就是Access-Control-Expose-Headers的作用。

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
複製程式碼

Access-Control-Max-Age欄位,設定預檢測請求能快取多久,單位秒。

Access-Control-Max-Age: <delta-seconds>
複製程式碼

Access-Control-Allow-Credentials欄位,指定了當瀏覽器的credentials設定為true時是否允許瀏覽器讀取response的內容

Access-Control-Allow-Credentials: true
複製程式碼

Access-Control-Allow-Methods欄位用於預檢請求的響應。其指明瞭實際請求所允許使用的 HTTP 方法。

Access-Control-Allow-Methods: <method>[, <method>]*
複製程式碼

Access-Control-Allow-Headers 首部欄位用於預檢請求的響應。其指明瞭實際請求中允許攜帶的首部欄位。

Access-Control-Allow-Headers: <field-name>[, <field-name>]*
複製程式碼

4.相容性

目前除了IE9及以下瀏覽不支援CORS外,其他瀏覽器基本的已經支援CORS。對於IE9及以下的瀏覽器可通過 XDomainRequest實現。

以上是我對CORS知識的梳理和記錄,因為在以往工作中頻繁遇到跨域問題,思來想去決定記錄下來,為確保資訊的正確性,極大的參照來官方文件,希望對有需要的小夥伴在處理跨域問題上有所幫助。

宣告:本文的內容參考於MDN官方文件,包括上面用到的事例程式碼也是借用自MDN,若有侵權,請聯絡我刪除相關內容。

相關文章