【計網-HTTP-03】HTTP/1.1細節之CORS
這一系列的七篇文章是由一篇拆分而來的,完整版請訪問Juneblog,從 HTTP/0.9 到 HTTP/3,記錄了關於 HTTP 的大部分問題.
CORS
在前後端分離開發時,你也許遇到過類似這樣的報錯:
Access to XMLHttpRequest at '*' from origin '*' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
這就是 CORS 的問題了,所謂 CORS (Cross-Origin Resource Sharing,跨域資源共享),它首先是一個系統,由一系列 HTTP 頭組成,這些 HTTP 頭決定了瀏覽器是否阻止前端 JavaScript 程式碼獲取跨域請求的響應。
之所以需要 CORS,是由於瀏覽器的同源安全策略:
同源安全策略
同源安全策略用來限制一個源(origin)的文件或者它載入的指令碼如何能與另一個源的資源進行互動。它能幫助阻隔惡意文件,減少可能被攻擊的媒介。
只有兩個 URL 的協議,主機,埠都相同時,他們才被認為是“同源的”,反之,如:http://www.a.com
和 https://www.a.com
原理
CORS 的原理很簡單,它通過新增一組 HTTP 頭,允許伺服器宣告哪些源站通過瀏覽器有許可權訪問哪些資源。另外,規範要求,對那些可能對伺服器資料產生副作用的 HTTP 請求方法(非簡單請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求,從而獲知服務端是否允許該跨源請求。伺服器確認允許之後,才發起實際的 HTTP 請求。在預檢請求的返回中,伺服器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 或 HTTP 認證相關資料)。
上面說到的 “可能對伺服器資料產生副作用的 HTTP 請求” 就是非簡單請求(not-so-simple request),與之對應的是簡單請求(simple request),同時滿足以下幾個條件的,屬於簡單請求。
-
請求方法是以下三種方法之一:
- HEAD
- GET
- POST
-
首部欄位只包含被使用者代理自動設定的首部欄位(例如 Connection ,User-Agent)和允許人為設定的欄位為 Fetch 規範定義的 對 CORS 安全的首部欄位集合。該集合為:
-
Accept
-
Accept-Language
-
Content-Language
-
Content-Type, 只限於三個值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
-
DPR
-
Downlink
-
Save-Data
-
Width
-
Viewport-Width
-
-
請求中的任意
XMLHttpRequestUpload
物件均沒有註冊任何事件監聽器;XMLHttpRequestUpload
物件可以使用XMLHttpRequest.upload
屬性訪問。 -
請求中沒有使用
ReadableStream
物件。
只要有其一不滿足,就是費簡單請求,非簡單請求在正式請求之前會先使用 OPTION
方法像伺服器發起一個 預檢請求,如下面這個請求:
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);
}
}
當前域為 foo.example.com
,請求 bar.other
, 屬於跨域請求,並且請求時自己添加了一個請求頭 X-PINGOTHER
,並且 Content-Type
型別為 application/xml
, 所以它屬於一個非簡單請求,在實際請求之前需要使用 OPTION
方法發一個預檢請求:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
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://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
預檢請求頭頭中最重要的部分有下面幾個:
Host
: 要請求的域Origin
: 發起請求的域,Host
和Origin
不一樣,說明是跨域請求Access-Control-Request-Method
: 正式的請求將要使用的方法Access-Control-Request-Headers
: 正式請求將攜帶的自定義欄位
伺服器在收到這樣的預檢請求後就可以根據請求頭決定是否允許即將傳送的實際請求,在伺服器的響應中,最重要的欄位有以下幾個:
Access-Control-Allow-Origin
: 伺服器允許的域,允許所有域該值設定為*
Access-Control-Allow-Methods
: 伺服器允許的請求方法,允許所有方法設定為*
Access-Control-Allow-Headers
: 伺服器允許的請求頭Access-Control-Max-Age
: 該響應的有效時間為 86400 秒,也就是 24 小時。在有效時間內,瀏覽器無須為同一請求再次發起預檢請求。
接受到響應後,瀏覽器會自動判斷實際請求是否被允許,如果不被允許,將會報上面的錯誤。
對於簡單請求,通過請求中的 Origin
和響應中的 Access-Control-Allow-Origin
就可以實現簡單的訪問控制,如果請求的 Origin
不在許可範圍內,伺服器會返回一個正常的響應,瀏覽器發現這個響應的頭資訊沒有包含Access-Control-Allow-Origin
欄位,就知道出錯了,從而丟擲一個錯誤,被XMLHttpRequest
的onerror
回撥函式捕獲。注意,這種錯誤無法通過狀態碼識別,因為HTTP迴應的狀態碼有可能是200。