1. 程式人生 > 其它 >【計網-HTTP-03】HTTP/1.1細節之CORS

【計網-HTTP-03】HTTP/1.1細節之CORS

技術標籤:httpwebcors

這一系列的七篇文章是由一篇拆分而來的,完整版請訪問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.comhttps://www.a.com

則會被認為是不同源的(協議不同),在預設情況下,同源策略會阻止通過不同源的URL獲取資源,而 CORS 就是提供了一種機制,以允許不同源的資源進行共享。

原理

CORS 的原理很簡單,它通過新增一組 HTTP 頭,允許伺服器宣告哪些源站通過瀏覽器有許可權訪問哪些資源。另外,規範要求,對那些可能對伺服器資料產生副作用的 HTTP 請求方法(非簡單請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求,從而獲知服務端是否允許該跨源請求。伺服器確認允許之後,才發起實際的 HTTP 請求。在預檢請求的返回中,伺服器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 或 HTTP 認證相關資料)。

上面說到的 “可能對伺服器資料產生副作用的 HTTP 請求” 就是非簡單請求(not-so-simple request),與之對應的是簡單請求(simple request),同時滿足以下幾個條件的,屬於簡單請求。

  1. 請求方法是以下三種方法之一:

    • HEAD
    • GET
    • POST
  2. 首部欄位只包含被使用者代理自動設定的首部欄位(例如 Connection ,User-Agent)和允許人為設定的欄位為 Fetch 規範定義的 對 CORS 安全的首部欄位集合。該集合為:

    • Accept

    • Accept-Language

    • Content-Language

    • Content-Type, 只限於三個值application/x-www-form-urlencodedmultipart/form-datatext/plain

    • DPR

    • Downlink

    • Save-Data

    • Width

    • Viewport-Width

  3. 請求中的任意XMLHttpRequestUpload 物件均沒有註冊任何事件監聽器;XMLHttpRequestUpload 物件可以使用 XMLHttpRequest.upload 屬性訪問。

  4. 請求中沒有使用 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: 發起請求的域,HostOrigin 不一樣,說明是跨域請求
  • 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欄位,就知道出錯了,從而丟擲一個錯誤,被XMLHttpRequestonerror回撥函式捕獲。注意,這種錯誤無法通過狀態碼識別,因為HTTP迴應的狀態碼有可能是200。