跨域資源共享——CORS

無名小貝勒發表於2018-11-25

轉載 跨域資源共享——CORS

跨域資源共享(Cross-Origin Resource Sharing)是一種機制,它使用額外的 HTTP 頭部告訴瀏覽器可以讓一個web應用進行跨域資源請求。

請求型別

簡單請求

若一個請求同時滿足下述所有條件,則該請求可視為“簡單請求”(注:灰色字型內容瞭解即可)

  • 使用的方法為
    • GET
    • HEAD
    • POST
  • 手動設定的頭部欄位只能是(注意:也可以設定 Forbidden header name 中的頭部欄位,如 ConnectionAccept-Encoding等,但是設定無效)
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(值的範圍還要符合下面的要求)
    • `DPR`
    • `Downlink`
    • `Save-Data`
    • `Viewpoer-Width`
    • `Width`
  • Content-Type 的值只能為
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • No event listeners are registered on any `XMLHttpRequestUpload` object used in the request; these are accessed using the XMLHttpRequest.upload property.
  • No ReadableStream object is used in the request.

預檢請求

CORS 預檢請求發生在實際請求之前,用於檢查伺服器是否支援 CORS,以判斷實際請求傳送是否安全。預檢請求使用的方式是 OPTIONS

當一個請求不是“簡單請求”時,即應該先傳送預檢請求,比如:

  • 這個請求的請求方式不是GETHEADPOST
  • 或者,這個請求設定了自定義的頭部欄位,比如 X-xxx
  • 或者這個請求的 Content-Type 值不是 application/x-www-form-urlencodedmultipart/form-datatext/plain,等等

跨域請求過程

跨域請求,CORS要求服務端設定一些頭部欄位,最重要的一個就是 Access-Control-Allow-Origin。下面以案例進行說明,前端使用 axios 進行 http 傳輸,後端以 koa 作為服務端框架,並使用CORS中介軟體 koa2-cors

簡單跨域請求

// Client http://localhost:8080
simpleRequest() {
  axios({
    method: 'GET',
    url: 'http://localhost:3000/api/simple'
  }).then(data => {
    console.log(data);
  });
}
複製程式碼複製程式碼
// Server http://localhost:3000
app.use(cors());
router.get('/api/simple', ctx => {
  ctx.body = { result: 'simple request success' };
});
複製程式碼複製程式碼

跨域資源共享——CORS

HTTP 報文:

跨域資源共享——CORS

HTTP 請求頭部有個 Origin 欄位,表示請求來自哪裡。HTTP 響應頭部中的 Access-Control-Allow-Origin 表示哪個域可以訪問該資源。使用 OriginAccess-Control-Allow-Origin 就完成了最簡單的訪問控制。

預檢請求&正式請求

// Client http://localhost:8080
mainRequest() {
  axios({
    method: 'POST',
    url: 'http://localhost:3000/api/mainRequest',
    headers: { 'X-test': 'CORS' } // 增加一個自定義的頭部欄位,觸發預檢請求
  }).then(data => {
    console.log(data);
  });
}
複製程式碼複製程式碼
// Server http://localhost:3000
app.use(cors());
router.post('/api/mainRequest', ctx => {
  ctx.body = { result: 'main request success' };
});
複製程式碼複製程式碼

跨域資源共享——CORS

預檢請求的報文:

跨域資源共享——CORS

請求首部欄位 Access-Control-Request-Method 告知伺服器,實際請求將使用 POST 方法。 請求首部欄位 Access-Control-Request-Headers 告知伺服器,實際請求將攜帶一個自定義請求首部欄位:x-test。伺服器據此決定,該實際請求是否被允許。

響應首部欄位 Access-Control-Allow-Methods 表明伺服器允許客戶端使用哪些方法發起請求。 響應首部欄位 Access-Control-Allow-Headers 表明伺服器允許請求中攜帶欄位 x-test。

實際請求的報文:

跨域資源共享——CORS

實際請求中傳送了 X-test 頭部欄位,響應狀態碼 200 OK。

可以看到,預檢請求中 Client 和 Server 使用了更多的頭部欄位來完成訪問控制。那麼,CORS 相關的請求頭部欄位和響應頭部欄位共有哪些呢?

頭部欄位

HTTP 請求頭部欄位

  • Origin
    Origin 頭部欄位表示預檢請求或實際請求的源站。
  • Access-Control-Request-Method
    Access-Control-Request-Method 頭部欄位用於預檢請求。其作用是,將實際請求所使用的 HTTP 方法告訴伺服器。
  • Access-Control-Request-Headers
    Access-Control-Request-Headers 頭部欄位用於預檢請求。其作用是,將實際請求所攜帶的首部欄位告訴伺服器。

注意,以上請求頭部欄位無須手動設定,當使用 XMLHttpRequest 物件發起跨域請求時,它們已經被設定就緒。

HTTP 響應頭部欄位

  • Access-Control-Allow-Origin
    其語法如下:

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

    origin 引數的值指定了允許訪問該資源的外域 URI。如果該欄位的值為萬用字元 *,則表示允許來自所有域的請求。
    注意,如果服務端指定了具體的域名而非 *,那麼響應頭部中的 Vary 欄位的值必須包含 Origin。這將告訴客戶端:伺服器對不同的源站返回不同的內容。

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

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

  • Access-Control-Expose-Headers
    跨域請求中,瀏覽器預設情況下通過API只能獲取到以下響應頭部欄位:

    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma

    如果想要訪問其他響應頭部資訊,則需要在伺服器端設定 Access-Control-Allow-HeadersAccess-Control-Expose-Headers 讓伺服器把允許瀏覽器訪問的頭部欄位放入白名單,比如:

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

    這樣瀏覽器就能夠訪問到 X-My-Custom-HeaderX-Another-Custom-Header 響應頭部了。

  • Access-Control-Max-Age
    Access-Control-Max-Age 欄位指定了預檢請求的結果能夠被快取多久,單位是 ,比如:

    Access-Control-Max-Age: 5
    複製程式碼複製程式碼

    表示在第一次預檢請求發出後,5s 內再訪問該介面時會直接傳送實際請求,而不需要先發預檢請求。過了 5s 後,會再要求先傳送預檢請求,以此類推。

    app.use(
      cors({
        maxAge: 5
      })
    );
    複製程式碼複製程式碼

    服務端設定了 5s 快取,實際請求如下:

    跨域資源共享——CORS

    注意,如果設定快取後,發現每次還是會傳送 OPTIONS 請求,請檢查你是不是勾選了“禁止快取”。

    跨域資源共享——CORS

  • Access-Control-Allow-Credentials
    XMLHttpRequest.withCredentials (或者 Request.credentials)表示跨域請求中,user agent 是否應該傳送 cookies、authorization headers 或者 TLS client certificates 等憑據。 Access-Control-Allow-Credentials 的作用就是:當 credentials 為 “真” 時(XHR和Fetch設定方式不一樣),Access-Control-Allow-Credentials 告訴瀏覽器是否把響應內容暴露給前端 JS 程式碼。比如:

    // Client http://localhost:8080
    simpleRequest() {
      axios({
        method: 'GET',
        url: 'http://localhost:3000/api/simple',
        withCredentials: true // 增加了withCredentials 選項
      }).then(data => {
        console.log(data);
      });
    }  
    複製程式碼

    // Server http://localhost:3000 app.use( cors({ maxAge: 5, // credentials: true }) ); 複製程式碼複製程式碼

    此時,服務端未設定 credentials: true,發起請求能看到客戶端報錯:

    跨域資源共享——CORS

    如果服務端設定了 credentials: true 則客戶端就不會報錯了。

    預檢請求的時候,Access-Control-Allow-Credentials 響應頭部欄位表示實際請求中是否可以使用 credentials。

關於 CORS 響應頭部欄位的運用,建議看一下 koa2-cors 中介軟體的原始碼。程式碼只有幾十行,特別清晰易懂。


CORS 相關內容如上,瞭解之後能更好地幫助我們解決日常聯調中出現的問題,比如:出現跨域了服務端怎麼設定,axios.post 方法傳送一個物件時為什麼會出現 OPTIONS 請求,代理伺服器怎麼才能轉發cookies等等。

相關文章