CORS 詳解,終于不用擔(dān)心跨域問(wèn)題了

CORS 是一個(gè)W3C標(biāo)準(zhǔn),全稱(chēng)是跨域資源共享(Cross-Origin Resource Sharing),也有譯為“跨源資源共享”的。

它允許瀏覽器向跨源服務(wù)器,發(fā)出 XMLHttpRequest(XHR) 或 Fetch API 跨域 HTTP 請(qǐng)求,從而克服了同源使用的限制。

本文內(nèi)容主要參考于跨域資源共享 CORS 詳解MDN 相關(guān)文檔。

一、簡(jiǎn)介

CORS 是 HTTP 的一部分,它允許服務(wù)端來(lái)指定哪些主機(jī)可以從這個(gè)服務(wù)端加載資源。

CORS 需要瀏覽器和服務(wù)器同時(shí)支持。目前,所有瀏覽器都支持該功能,IE 瀏覽器不能低于 IE10。

整個(gè) CORS 通信過(guò)程,都是瀏覽器自動(dòng)完成,不需要用戶(hù)參與。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),CORS 通信與同源的 AJAX 通信沒(méi)有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn) AJAX 請(qǐng)求跨源,就會(huì)自動(dòng)添加一些附件的頭信息,有時(shí)還會(huì)多處一次附件的請(qǐng)求,但用戶(hù)不會(huì)有感覺(jué)。

因此,實(shí)現(xiàn) CORS 通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實(shí)現(xiàn)了 CORS 接口(響應(yīng)報(bào)文包括了正確的 CORS 響應(yīng)頭),就可以跨源通信。

二、同源安全策略

同源策略是一個(gè)重要的安全策略,它用于限制一個(gè) Origin 的文檔或它加載的腳本如何能與另一個(gè)源的資源進(jìn)行交互。它能幫助阻隔惡意文檔,減少可能被攻擊的媒介。

如果兩個(gè) URL 的協(xié)議(Protocol)、主機(jī)(Host)、端口(Port,如果有指定的話(huà))都相同的話(huà),那么這兩個(gè) URL 是同源的,否則是不同源的。

下表給出了與 URL http://store.company.com/dir/page.html 的源進(jìn)行對(duì)比的示例:

URL 結(jié)果 原因
http://store.company.com/dir2/other.html 同源 只有路徑不同。
http://store.company.com/dir/inner/another.html 同源 只有路徑不同。
https://store.company.com/secure.html 不同源 協(xié)議不同。
http://store.company.com:81/dir/etc.html 不同源 端口不同(http:// 默認(rèn)端口是 80)。
http://news.company.com/dir/other.html 不同源 主機(jī)不同。

關(guān)于 IE 瀏覽器的特例,其中差異點(diǎn)是不規(guī)范的,其他瀏覽器未做出支持。

出于安全性,瀏覽器限制腳本內(nèi)發(fā)起的跨域 HTTP 請(qǐng)求,例如常見(jiàn)的 XHR、Fetch API 都遵循同源策略。

三、兩種請(qǐng)求

瀏覽器將 CORS 請(qǐng)求分成兩類(lèi):簡(jiǎn)單請(qǐng)求(simple request)和非簡(jiǎn)單請(qǐng)求(not-so-simple request)。

若滿(mǎn)足下述所有條件,則該請(qǐng)求可視為“簡(jiǎn)單請(qǐng)求”:

  • 請(qǐng)求方法是以下三種之一:
    • HEAD
    • GET
    • POST
  • HTTP 的頭信息不超出以下幾種字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 的值僅限于以下三種之一
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 請(qǐng)求中的任意 XMLHttpRequestUpload 對(duì)象均沒(méi)有注冊(cè)任何事件監(jiān)聽(tīng)器。
  • 請(qǐng)求中沒(méi)有使用 ReadableStream 對(duì)象。

凡是不同時(shí)滿(mǎn)足以上條件,均屬于非簡(jiǎn)單請(qǐng)求。

注意: 這些跨站點(diǎn)請(qǐng)求與瀏覽器發(fā)出的其他跨站點(diǎn)請(qǐng)求并無(wú)二致。如果服務(wù)器未返回正確的響應(yīng)首部,則請(qǐng)求方不會(huì)收到任何數(shù)據(jù)。因此,那些不允許跨站點(diǎn)請(qǐng)求的網(wǎng)站無(wú)需為這一新的 HTTP 訪(fǎng)問(wèn)控制特性擔(dān)心。

三、簡(jiǎn)單請(qǐng)求

一個(gè)簡(jiǎn)單的 XHR 請(qǐng)求示例:

客戶(hù)端(Client):

// Client 客戶(hù)端(http://192.168.1.105:8080)
function xhrRequest() {
  // 創(chuàng)建 xhr 對(duì)象
  const xhr = new XMLHttpRequest()

  // 通過(guò) onreadystatechange 事件捕獲請(qǐng)求狀態(tài)的變化
  // 必須在 open 之前指定該事件,否則無(wú)法接收 readyState 0 和 1 的狀態(tài)
  xhr.onreadystatechange = () => {
    console.log(xhr.status)
    console.log(xhr.readyState)
  }

  // 捕獲錯(cuò)誤
  xhr.onerror = err => {
    console.log('error:', err)
  }

  // 初始化請(qǐng)求
  xhr.open('GET', 'http://192.168.1.105:7701/config', true)

  // 發(fā)送請(qǐng)求
  xhr.send(null)
}

xhrRequest()

服務(wù)端(Server):

// Server 服務(wù)端(通過(guò) node 命令即可啟動(dòng),http://192.168.1.105:7701)
const http = require('http')
const port = 7701
const allowedOrigin = 'http://192.168.1.105:8080' // 這個(gè)是我本地客戶(hù)端的 Origin(源)

const server = http.createServer((request, response) => {
  if (request.headers.origin === allowedOrigin) {
    response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
  }
  if (request.url === '/config') {
    response.end(JSON.stringify({ name: 'Frankie', age: 20 }))
  }
})

server.listen(port)

發(fā)起 XHR HTTP 請(qǐng)求,我們可以在控制臺(tái)看到請(qǐng)求報(bào)文和響應(yīng)報(bào)文。(View source)

1. 基本流程

對(duì)于簡(jiǎn)單請(qǐng)求,瀏覽器直接發(fā)出 CORS 請(qǐng)求。具體來(lái)說(shuō),就在頭信息之中,增加一個(gè) Origin 字段。

通過(guò)以上示例發(fā)起 HTTP GET 請(qǐng)求,請(qǐng)求報(bào)文如下:

GET /config HTTP/1.1
Host: 192.168.1.105:7701
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1
Accept: */*
Origin: http://192.168.1.105:8080
Referer: http://192.168.1.105:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7

上面的頭信息中,Origin 字段用來(lái)說(shuō)明,本次請(qǐng)求來(lái)自哪個(gè)源(協(xié)議 + 主機(jī) + 端口)。服務(wù)器根據(jù)這個(gè)值,絕對(duì)是否同意這次請(qǐng)求。

如果 Origin 指定的源,不在許可范圍內(nèi),服務(wù)器會(huì)返回一個(gè)正確的 HTTP 回應(yīng)。瀏覽器發(fā)現(xiàn),這個(gè)回應(yīng)的頭信息沒(méi)有包含 Access-Control-Allow-Origin 字段(相見(jiàn)下文),就知道出錯(cuò)了,從而拋出一個(gè)錯(cuò)誤,被 XMLHttpRequestonerror 回調(diào)函數(shù)捕獲。注意,這種錯(cuò)誤無(wú)法通過(guò)狀態(tài)識(shí)別,因?yàn)?HTTP 回應(yīng)的狀態(tài)碼有可能是 200。

在上述服務(wù)端示例中,我們指定的源就是 http://192.168.1.105:8080,因此可以正常發(fā)起請(qǐng)求,并拿到接口響應(yīng)數(shù)據(jù):{"name":"Frankie","age":20}。

假設(shè),我們將服務(wù)端示例許可的 Origin 去掉:

// if (request.headers.origin === allowedOrigin) {
//  response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
// }

我們?cè)俅伟l(fā)起 /config 請(qǐng)求的時(shí)候,可以看到控制臺(tái)拋出跨域錯(cuò)誤了:

需要注意的是,盡管無(wú)論從控制臺(tái)看到的報(bào)錯(cuò)、還是 Network 選項(xiàng)卡的 Failed to load response data,都可以知道我們的請(qǐng)求失敗了。但......其實(shí)是這樣的:就本文服務(wù)端示例而言,即使是跨域請(qǐng)求,也是會(huì)返回響應(yīng)數(shù)據(jù)的,只是 JavaScript 腳本獲取到的結(jié)果是“失敗”而已。

那不對(duì)啊,如果服務(wù)器正常返回?cái)?shù)據(jù),而前端卻失敗了,那信息不對(duì)稱(chēng)啊,中間商賺差價(jià)了?怎么回事呢?原因是瀏覽器在中間搞鬼,那瀏覽器在中間扮演了什么角色呢?

當(dāng)發(fā)起 CORS 請(qǐng)求時(shí),瀏覽器首先會(huì)在請(qǐng)求報(bào)文上自動(dòng)加上 Origin 的字段(它的值由當(dāng)前頁(yè)面的 Protocol + Host + Port 部分組成),到達(dá)服務(wù)端之后,會(huì)做出相應(yīng)的處理并返回?cái)?shù)據(jù)。由于服務(wù)端并沒(méi)有給響應(yīng)報(bào)文的頭部設(shè)置 Access-Control-Allow-Origin(前面說(shuō)把這塊注釋掉了),自然瀏覽器接收到的響應(yīng)報(bào)文中就不含 Access-Control-Allow-Origin,當(dāng)瀏覽器就判斷到請(qǐng)求報(bào)文與響應(yīng)報(bào)文的 Origin 不相等,此時(shí)它不會(huì)將服務(wù)器的響應(yīng)數(shù)據(jù) JavaScript 腳本,即我們的 XHR 對(duì)象無(wú)法得到服務(wù)器的響應(yīng)數(shù)據(jù),并且會(huì)觸發(fā) XHR 對(duì)象的 onerror 事件,讓腳本來(lái)捕獲錯(cuò)誤。所以,我們就看到請(qǐng)求失敗了。(這種情況下,可以通過(guò)抓包工具查看服務(wù)器返回的數(shù)據(jù))

還有,只有在 Origin 指定的源在許可范圍內(nèi),服務(wù)器響應(yīng)報(bào)文才會(huì)多出這些 Access-Control-Allow-Origin、Access-Control-Allow-Credentials、Access-Control-Expose-Headers 頭信息字段。(可從客戶(hù)端 Network 選項(xiàng)卡觀(guān)察到)

以下與 CORS 請(qǐng)求相關(guān)的頭信息字段,都以 Access-Control- 開(kāi)頭。

Access-Control-Allow-Origin: http://10.16.4.226:8080
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Date
  • Access-Control-Allow-Origin

這個(gè)字段是必須的。它的值要么是請(qǐng)求時(shí) Origin 字段的值,要么是一個(gè) *,表示接受任意的源(域名)的請(qǐng)求。

  • Access-Control-Allow-Credentials

這個(gè)字段可選。它的值是一個(gè)布爾值,表示是否允許發(fā)送 Cookie。默認(rèn)情況下,Cookie 不包括在 CORS 請(qǐng)求之中。設(shè)為 true,即表示服務(wù)器明確許可,Cookie 可以包含在請(qǐng)求中,一起發(fā)給服務(wù)器。這個(gè)值也只能設(shè)為 true,如果服務(wù)器不要瀏覽器發(fā)送 Cookie,刪除該字段即可。

// Client
xhr.withCredentials = true

// Server
response.setHeader('Access-Control-Allow-Credentials', true)

當(dāng)客戶(hù)端攜帶 Cookie 發(fā)起請(qǐng)求,同時(shí)服務(wù)端允許攜帶 Cookie 的情況下,可以看到請(qǐng)求報(bào)文包含了 Cookie 信息:

Cookie: username=Frankie
  • Access-Control-Expose-Headers

這個(gè)字段可選。CORS 請(qǐng)求時(shí),XMLHttpRequest 對(duì)象的 getResponseHeader() 方法只能拿到 6 個(gè)基本字段: Cache-Control、Content-Language、Content-Type、ExpiresLast-Modified、Pragma。如果想要拿到其他字段,就必須在 Access-Control-Expose-Headers 里面指定。(但由于 W3C 的限制,不指定的情況下,客戶(hù)端獲取到的值可能為 null

例如,服務(wù)端將 Access-Control-Expose-Headers 指定為 "Date,Access-Control-Allow-Origin",我們可以通過(guò) XMLHttpRequest 對(duì)象的 getResponseHeader() 方法獲取對(duì)應(yīng)字段的值。

// Client
xhr.onreadystatechange = () => {
  if (xhr.status === 200 && xhr.readyState === 4) {
    console.log(xhr.getResponseHeader('Cache-Control')) // null
    console.log(xhr.getResponseHeader('Content-Language')) // null
    console.log(xhr.getResponseHeader('Content-Type')) // null
    console.log(xhr.getResponseHeader('Expires')) // null
    console.log(xhr.getResponseHeader('Last-Modified')) // null
    console.log(xhr.getResponseHeader('Pragma')) // null
    console.log(xhr.getResponseHeader('Date')) // "Sun, 06 Jun 2021 14:34:34 GMT"
    console.log(xhr.getResponseHeader('Access-Control-Allow-Origin')) // "http://192.168.1.105:8080"
    console.log(xhr.getResponseHeader('X-Custom-Header')) // Error: Refused to get unsafe header "X-Custom-Header"
  }
}

// Server
if (request.headers.origin === allowedOrigin) {
  response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
  response.setHeader('Access-Control-Allow-Credentials', true)
  response.setHeader('Access-Control-Expose-Headers', 'Date,Access-Control-Allow-Origin')
}

如果連接未完成,響應(yīng)中不存在報(bào)文項(xiàng),或者被 W3C 限制,則返回 null。假設(shè)如上示例,自定義頭信息 X-Custom-Header 字段,且服務(wù)端未對(duì)其進(jìn)行設(shè)置,則會(huì)拋出錯(cuò)誤:Refused to get unsafe header "X-Custom-Header"

2. withCredentials 屬性

上面說(shuō)到,CORS 請(qǐng)求默認(rèn)不發(fā)送 Cookie 和 HTTP 認(rèn)證信息。如果要把 Cookie 發(fā)送到服務(wù)器,一方面要服務(wù)器同意,指定 Access-Control-Allow-Credentials 字段。

Access-Control-Allow-Credentials: true

另一方面,開(kāi)發(fā)者必須在 AJAX 請(qǐng)求中打開(kāi) withCredentials 屬性。

xhr.withCredentials = true

否則,即使服務(wù)器同意發(fā)送 Cookie,瀏覽器也不會(huì)發(fā)送。或者服務(wù)器要求設(shè)置 Cookie,瀏覽器也不會(huì)處理。

但是,如果省略 withCredentials 設(shè)置,有的瀏覽器還是會(huì)一起發(fā)送 Cookie。這時(shí),可以顯示關(guān)閉 withCredentials。

xhr.withCredentials = false

需要注意的是,如果要發(fā)送 Cookie,Access-Control-Allow-Origin 就不能設(shè)為 *(否則會(huì)拋出如下錯(cuò)誤),必須指定明確的、與請(qǐng)求網(wǎng)頁(yè)一致的域名。同時(shí),Cookie 依然遵循同源策略,只有用服務(wù)器域名設(shè)置的 Cookie 才會(huì)上傳,其他域名的 Cookie 并不會(huì)上傳,且(跨源)原網(wǎng)頁(yè)代碼中的 document.cookie 也無(wú)法讀取服務(wù)器域名下的 Cookie。

四、非簡(jiǎn)單請(qǐng)求

調(diào)整一下示例:

// Client 客戶(hù)端
function xhrRequest() {
  // 創(chuàng)建 xhr 對(duì)象
  const xhr = new XMLHttpRequest()

  // 通過(guò) onreadystatechange 事件捕獲請(qǐng)求狀態(tài)的變化
  xhr.onreadystatechange = () => {
    console.log(xhr.status)
    console.log(xhr.readyState)

    if (xhr.status === 200 && xhr.readyState === 4) {
      console.log(xhr.getResponseHeader('Date'))
      console.log(xhr.getResponseHeader('Access-Control-Allow-Origin'))
    }
  }

  // 捕獲錯(cuò)誤
  xhr.onerror = err => {
    console.log('error:', err)
  }

  // 初始化請(qǐng)求
  xhr.open('PUT', 'http://192.168.1.105:7701/config', true)

    // 設(shè)置自定義頭部(必須在 open 之后)
    xhr.setRequestHeader('X-Custom-Header', 'foo')

  // 發(fā)送請(qǐng)求
  xhr.send(null)
}

xhrRequest()
// Server 服務(wù)端
const http = require('http')
const port = 7701
const allowedOrigin = 'http://192.168.1.105:8080' // 這個(gè)是我本地客戶(hù)端的 Origin(源)

const server = http.createServer((request, response) => {
  if (request.headers.origin === allowedOrigin) {
  response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
  response.setHeader('Access-Control-Allow-Credentials', true)
  response.setHeader('Access-Control-Allow-Methods', 'PUT')
  response.setHeader('Access-Control-Allow-Headers', 'X-Custom-Header')
  response.setHeader('Access-Control-Expose-Headers', 'Date,Access-Control-Allow-Origin')
  }
  if (request.url === '/config') {
    response.end(JSON.stringify({ name: 'Frankie', age: 20 }))
  }
})

server.listen(port)
1. 預(yù)檢請(qǐng)求

非簡(jiǎn)單請(qǐng)求時(shí)那種對(duì)服務(wù)器有特殊要求的請(qǐng)求,比如請(qǐng)求方法是 PUTDELETE,或者 Content-Type 字段的類(lèi)型是 application/json。

非簡(jiǎn)單請(qǐng)求的 CORS 請(qǐng)求,會(huì)在正式通信之前,增加一次 HTTP 查詢(xún)請(qǐng)求,稱(chēng)為預(yù)檢請(qǐng)求(preflight)。

瀏覽器先詢(xún)問(wèn)服務(wù)器,當(dāng)前網(wǎng)頁(yè)所在的域名是否在服務(wù)器的許可名單之中,以及可以使用哪些 HTTP 動(dòng)詞和頭信息字段。只有得到肯定答復(fù),瀏覽器才會(huì)發(fā)出正式的 XMLHttpRequest 請(qǐng)求,否則就報(bào)錯(cuò)。

上面代碼中,HTTP 請(qǐng)求的方法是 PUT,并且發(fā)送一個(gè)自定義頭信息 X-Custom-Header。

瀏覽器發(fā)現(xiàn),這是一個(gè)非簡(jiǎn)單請(qǐng)求,就自動(dòng)發(fā)出一個(gè)“預(yù)檢請(qǐng)求”,要求服務(wù)器確認(rèn)可以這樣請(qǐng)求。下面是這個(gè)“預(yù)檢請(qǐng)求”的 HTTP 頭信息。

OPTIONS /config HTTP/1.1
Host: 192.168.1.105:7701
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: x-custom-header
Origin: http://192.168.1.105:8080
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1
Sec-Fetch-Mode: cors
Referer: http://192.168.1.105:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7

“預(yù)檢請(qǐng)求”用的請(qǐng)求方法是 OPTIONS,表示這個(gè)請(qǐng)求是用來(lái)詢(xún)問(wèn)的。頭信息里面,關(guān)鍵字是 Origin,表示請(qǐng)求來(lái)自哪個(gè)源。

除了 Origin 字段,“預(yù)檢請(qǐng)求”的頭信息包括兩個(gè)特殊字段。

  • Access-Control-Request-Method
    這個(gè)字段是必須的,用來(lái)列出瀏覽器的 CORS 請(qǐng)求會(huì)用到哪些 HTTP 方法,上面示例是 PUT。

  • Access-Control-Request-Headers
    這個(gè)字段是一個(gè)逗號(hào) , 分隔的字符串,指定瀏覽器 CORS 請(qǐng)求會(huì)額外發(fā)送的頭信息字段,上面示例是 X-Custom-Header。

Tips:預(yù)檢請(qǐng)求可以在調(diào)試器 Network 選項(xiàng)卡中查看,如下圖:

2. 預(yù)檢請(qǐng)求的回應(yīng)

服務(wù)器收到“預(yù)檢請(qǐng)求”以后,檢查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段以后,確認(rèn)允許跨域請(qǐng)求,就可以做出回應(yīng)。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://192.168.1.105:8080
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Expose-Headers: Date,Access-Control-Allow-Origin
Date: Sun, 06 Jun 2021 15:27:27 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 27

上面的 HTTP 回應(yīng)中,關(guān)鍵的是 Access-Control-Allow-Origin 字段,表示 http://192.168.1.105:8080 可以請(qǐng)求數(shù)據(jù)。該字段也可以設(shè)為 *,表示同意任意的跨源請(qǐng)求。(本示例攜帶了 Cookie 信息,不可設(shè)為 *

Access-Control-Allow-Origin: *

如果服務(wù)器否定了“預(yù)檢請(qǐng)求”,會(huì)返回一個(gè)正常的 HTTP 回應(yīng),但是沒(méi)有任何的 CORS 相關(guān)的頭信息字段。這是,瀏覽器就認(rèn)定,服務(wù)器不同意預(yù)檢請(qǐng)求,因此觸發(fā)一個(gè)錯(cuò)誤,被 XMLHttpRequest 對(duì)象的 onerror 回調(diào)函數(shù)捕獲。

如果將服務(wù)端的 Access-Control-Allow-Headers 注釋掉:

// response.setHeader('Access-Control-Allow-Headers', 'X-Custom-Header')

控制臺(tái)會(huì)打印出如下的報(bào)錯(cuò)信息:

服務(wù)器回應(yīng)的其他 CORS 相關(guān)字段如下:

Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
  • Access-Control-Allow-Methods
    這個(gè)字段是必須的,它的值是逗號(hào) , 分隔的一個(gè)字符串,表明服務(wù)器支持的所有跨域請(qǐng)求。注意,返回的是所有支持的方法,而不單是瀏覽器請(qǐng)求的那個(gè)方法。這是為了避免多次“預(yù)檢請(qǐng)求”。(本示例僅設(shè)置了一個(gè) PUT 方法。)

  • Access-Control-Allow-Headers
    如果瀏覽器請(qǐng)求包括 Access-Control-Request-Headers 字段,則 Access-Control-Allow-Headers 字段是必須的,它也是一個(gè)逗號(hào) , 分隔的字符串,表明服務(wù)器支持的所有頭信息字段,不限于瀏覽器在“預(yù)檢請(qǐng)求”的字段。

  • Access-Control-Allow-Credentials
    這個(gè)字段與簡(jiǎn)單請(qǐng)求時(shí)的含義相同。

  • Access-Control-Max-Age
    這個(gè)字段可選,用來(lái)指定本次預(yù)檢請(qǐng)求的有效期,單位為秒。上面結(jié)果中,有效期是 20 天(1728000 秒),即允許緩存該條回應(yīng) 1728000 秒(即 20 天),在此期間,不用發(fā)出另一條預(yù)檢請(qǐng)求。

3. 瀏覽器的正常請(qǐng)求和回應(yīng)

一旦服務(wù)器通過(guò)了“預(yù)檢請(qǐng)求”,以后每次瀏覽器正常的 CORS 請(qǐng)求,就都跟簡(jiǎn)單請(qǐng)求一樣,會(huì)有一個(gè) Origin 頭信息字段。服務(wù)器的回應(yīng),也都會(huì)有一個(gè) Access-Control-Allow-Origin 頭信息字段。

下面是“預(yù)檢請(qǐng)求”之后,瀏覽器的正常 CORS 請(qǐng)求的請(qǐng)求報(bào)文。

PUT /config HTTP/1.1
Host: 192.168.1.105:7701
Connection: keep-alive
Content-Length: 0
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1
X-Custom-Header: foo
Accept: */*
Origin: http://192.168.1.105:8080
Referer: http://192.168.1.105:8080/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: username=Frankie

上面的頭信息的 Origin 字段是瀏覽器自動(dòng)添加的。

下面是服務(wù)器正常的響應(yīng)報(bào)文。

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://192.168.1.105:8080
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Expose-Headers: Date,Access-Control-Allow-Origin
Date: Sun, 06 Jun 2021 15:40:11 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 27

上面的頭信息中,Access-Control-Allow-Origin 字段是每次回應(yīng)都必定包含的。

五、與 JSONP 的比較

CORS 與 JSONP 的使用目的相同,但是比 JSONP 更強(qiáng)大。

JSONP 只支持 GET 請(qǐng)求,CORS 支持所有類(lèi)型的 HTTP 請(qǐng)求。JSONP 的優(yōu)勢(shì)在于支持老式瀏覽器,以及可以向不支持 CORS 的網(wǎng)站請(qǐng)求數(shù)據(jù)。

六、其他

以下內(nèi)容可能與 CORS 無(wú)關(guān),不想看可跳過(guò)。

1. HTTP headers 之 Referer

Referer 請(qǐng)求頭包含了當(dāng)前請(qǐng)求頁(yè)面的來(lái)源頁(yè)面的地址,即表示當(dāng)前頁(yè)面是通過(guò)此來(lái)源頁(yè)面里的鏈接進(jìn)入的。服務(wù)端一般使用 Referer 請(qǐng)求頭識(shí)別訪(fǎng)問(wèn)來(lái)源,可能會(huì)以此進(jìn)行統(tǒng)計(jì)分析、日志記錄以及緩存優(yōu)化等。

注意,如果直接在地址欄輸入 URL 訪(fǎng)問(wèn)某網(wǎng)頁(yè),這時(shí) Referer 為空。

例如,上面示例中請(qǐng)求 /config 接口的網(wǎng)頁(yè)地址是 http://192.168.1.105:8080/#/about,從請(qǐng)求報(bào)文可以大致看到 OriginReferer 的區(qū)別。

Origin: http://192.168.1.105:8080
Referer: http://192.168.1.105:8080/
  • Origin
    Origin 的值可以置空或者是 <scheme>://<host> [:<port>],即協(xié)議 + 主機(jī) + 端口。其中主機(jī)是指域名或者 IP 地址;端口號(hào)可選,缺省時(shí)為服務(wù)的默認(rèn)端口(例如 HTTP 請(qǐng)求默認(rèn)端口是 80,HTTPS 請(qǐng)求默認(rèn)端口是 443

  • Referer
    當(dāng)前頁(yè)面被鏈接而至的前一頁(yè)面的絕對(duì)路徑或者相對(duì)路徑。不包含 URL fragments (例如 "#section") 和 userinfo (例如 "https://username:password@example.com/foo/bar/" 中的 "username:password" )。

上面的解釋比較官方,換句話(huà)說(shuō)就是從哪個(gè)頁(yè)面鏈接過(guò)來(lái)的。例如,我從 GitHub 倉(cāng)庫(kù)點(diǎn)擊跳轉(zhuǎn)至簡(jiǎn)書(shū)文章,可以從 HTML 的請(qǐng)求報(bào)文看到:

Host: www.itdecent.cn
Referer: https://github.com/toFrankie/csscomb-mini

假設(shè)有一個(gè)需求是統(tǒng)計(jì)從 GitHub 訪(fǎng)問(wèn)簡(jiǎn)書(shū)的訪(fǎng)問(wèn)量,那么就可以通過(guò) Referer 來(lái)實(shí)現(xiàn)。也常用于防盜鏈等。

需要注意的是,Referer 的正確英語(yǔ)拼法是 referrer。由于早期 HTTP 規(guī)范的拼寫(xiě)錯(cuò)誤,為保持向下兼容就將錯(cuò)就錯(cuò)了。例如 DOM Level 2、Referrer Policy 等其他網(wǎng)絡(luò)技術(shù)的規(guī)范曾試圖修正此問(wèn)題,使用正確拼法,導(dǎo)致目前拼法并不統(tǒng)一。

七、參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最近在琢磨前后端分離,難免會(huì)碰到跨域問(wèn)題。 首先弄清楚,跨域指的是瀏覽器不能執(zhí)行其他網(wǎng)站的腳本。它是由瀏覽器的同源...
    舒健Wilson閱讀 3,009評(píng)論 0 2
  • 0. 背景 瀏覽器中,網(wǎng)站A的網(wǎng)絡(luò)請(qǐng)求訪(fǎng)問(wèn)網(wǎng)站A的資源(圖片,HTTP請(qǐng)求)是很順暢的,而想訪(fǎng)問(wèn)網(wǎng)站B的資源,就要...
    張?jiān)骑wVir閱讀 1,061評(píng)論 0 1
  • CORS是一個(gè)W3C標(biāo)準(zhǔn),全稱(chēng)是"跨域資源共享"(Cross-origin resource sharing)。 ...
    起名字太累閱讀 1,010評(píng)論 0 2
  • 1.跨域問(wèn)題 1.1.什么是跨域 跨域是指跨域名的訪(fǎng)問(wèn),以下情況都屬于跨域: 如果域名和端口都相同,但是請(qǐng)求路徑不...
    2772f1692959閱讀 576評(píng)論 0 0
  • 1.什么是跨域 跨域是指跨域名的訪(fǎng)問(wèn),以下情況都屬于跨域: 跨域現(xiàn)象實(shí)例域名不相同www.baidu.com與ww...
    AnonyStar閱讀 364評(píng)論 0 0

友情鏈接更多精彩內(nèi)容