
遇到 CORS 怎麼辦 😫
找後端 (❌)
又稱跨來源資源共用,當我們使用 JavaScript 取得資源時,非同源的 request 會因為安全性考量而受到限制。瀏覽器會強制你遵守 CORS 的規範,否則 request 會失效。
什麼是同源?
依據 同源政策(Same-origin policy),兩個網頁具備相同協定(protocol)、埠號(port,如果有指定)以及網域(domain),則為同源。而非同源的網站產生的 request 即為跨來源請求(cross-origin http request)。以 https://www.terminal-420.space
來和下面幾個網址相比:
1. http://www.terminal-420.space // 通訊協定不同,非同源
2. https://www.terminal-420.com // domain 不同,非同源
3. https://www.cannabis.terminal-420.space // domain 不同,非同源
4. https://www.terminal-420.space:3080 // 埠號不同,非同源
5. https://www.terminal-420.space/blog // 同源
而來自於非同源的請求,就稱為跨來源請求。
什麼是 CORS?
CORS 是一種機制,其使用額外的 Http header,讓非同源的請求可以被管理、並安全的回覆該請求。
在 JavaScript 中,如果在 Server 沒有額外設定的情況下,所有非同源請求都會被阻擋。而在 CORS 的規範中,跨來源請求分為兩種,簡單(simple)和預檢(preflighted)請求
簡單請求(Simple request)
不需要經過 preflighted(或不觸發 preflighted)就可以直接發送的請求,在 fetch spec 當中稱為 non-preflighted request
。僅允許下面三個 HTTP Methods
- GET
- HEAD
- POST
除了 HTTP Methods 之外,簡單請求中只能包含特定的 header field。除了 user agent 自動設定的 header(如 Connection、User-Agent)之外,另外可以加上:
Accept
Accept-Language
(en-US)Content-Language
(en-US)Content-Type
(但請注意下方的額外要求)Last-Event-ID
DPR
Save-Data
Viewport-Width
Width
其中,Content-Type
只能包含 application/x-www-form-urlencoded
、multipart/form-data
、text/plain
這三種。因此下方的請求就不符合簡單請求的標準,因爲 Content-Type
為 application/json
const response = await fetch('https://othersite.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
如果該請求符合簡單請求的所有規範,那麼瀏覽器會向 Server 發送 request。如果我們在 https://www.terminal-420.space
向另一個 Server 發送一個 request,大概會長的像這樣:
GET /resources/public-data/ HTTP/1.1
Host: someserver.com
User-Agent: ...
Accept: ...
Accept-Language: ...
Accept-Encoding: ...
Accept-Charset: ...
Connection: ...
Referer: ...
Origin: https://www.terminal-420.space
在最後的 Origin
header 當中,標示了這個 request 從何而來。這個情境下是 https://www.terminal-420.space
發出請求。再來看看伺服器如何回應這個請求
HTTP/1.1 200 OK
Date: ...
Server: ...
Access-Control-Allow-Origin: *
Keep-Alive: ...
Connection: ...
Transfer-Encoding: ...
Content-Type: application/xml
在 Server 回傳的 header 當中有一個 Access-Control-Allow-Origin
,其值為 *
,代表他可以允許任何網站跨站請求資源。如果伺服器僅允許特定來源,像是 https://www.terminal-420.space
才能夠存取的話,那麼 Access-Control-Allow-Origin
這個 header 則會回傳:
Access-Control-Allow-Origin: https://www.terminal-420.space
而其他非 https://www.terminal-420.space
的站點都無法存取該資源。
預檢請求(Preflighted request)
其他各種非簡單請求的 request,都必須先經過「預檢(preflighted)」後,才能發送出實際的請求。在請求發送之後,會先經過 CORS check。當數個條件都符合時(其中一個是設置 use-CORS-preflight flag ),此時就會發送預檢請求(CORS-preflight fetch)
CORS-preflight fetch 透過 HTTP OPTIONS 方法來發送,用來確認伺服器可不可以接收這個請求。一個 Preflighted request 大概長得像這樣
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.example
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
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
其中最下方的 Access-Control-Request-Method
及 Access-Control-Request-Headers
分別包含了真正要請求的方法以及 headers。在這裡,我們真正想要發送的請求是 POST,其中包含了 X-PINGOTHER
以及 Content-Type
這兩個 headers。
如果這些都符合 Server 的設定,Server 就會回傳 200
OK 的 response
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: https://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
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
其中包含
Access-Control-Allow-Origin
:Server 允許進行 CORS 請求的 OriginsAccess-Control-Allow-Methods
:Server 允許的請求方法,在這裡包含POST
GET
及OPTIONS
Access-Control-Allow-Headers
:Server 允許的 HeadersAccess-Control-Max-Age
:這個 permissions 被快取的時間,這裡是 86400 秒,也就是一天整
而依據 MDN,Status code 200
和 204
No Content 都是允許的 status code,但有些瀏覽器似乎會因為 status 204
而不接著發送後續的 request。
Request with credentials
在預設情況下,跨域的 Fetch
或 XMLHttpRequest
都不會帶有 cookie,除非在 request 中帶入特定的 flag。在 Fetch
中需將 credentials
設置為 include
fetch('https://someserver.com/resources/public-data/', {
credentials: 'include'
})
而 XMLHttpRequest
則需帶入 withCredentials = true
const invocation = new XMLHttpRequest();
const url = "https://bar.other/resources/credentialed-content/";
function callOtherDomain() {
if (invocation) {
invocation.open("GET", url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
另外,無論是簡單請求或需要預檢的請求,若 Server 沒有在 response 中帶入 Access-Control-Allow-Credentials: true
的話,那麼瀏覽器端還是無法看到回傳的資訊。
CORS-preflight cache
CORS-preflight cache 是一串 list,裡面放著 cache 的實體。每一個 CORS-preflight cache 實體包含:
- key (a network partition key)
- byte-serialized origin (a byte sequence)
- URL (a URL)
- max-age (a number of seconds)
- credentials (a boolean)
- method (null,
*
, or a method) - header name (null,
*
, or a header name)
當 cache 實體建立完成後,就會加入到 user agent 的 CORS-preflight cache 當中。
若要移除 cache,會比對 key、byte-serialized origin 以及 URL,找到正確的 cache 後移除。
Express 的 CORS 做了什麼
終於搞懂 CORS 之後,來看看 Express 的 CORS 做了什麼!cors 是 express 的一個 middleware package,用來設置 CORS 的參數。
打開 cors 的 source code 一看,其實也才兩百多行,而且每一個 function 做的事都還蠻清楚的。不同 function 用來設置不同的 header,並以陣列形式儲存,最後用 join
將陣列中的值組合起來,或是回傳陣列繼續組合。
像是設置 Method 的 configureMethods
:
function configureMethods(options) {
var methods = options.methods;
if (methods.join) {
methods = options.methods.join(','); // .methods is an array, so turn it into a string
}
return {
key: 'Access-Control-Allow-Methods',
value: methods
};
}
在 express 裡面寫的這些設定,好像也就很好理解了!
const express = require('express')
const app = express()
const cors = require('cors')
app.use(cors({
origin: "http://127.0.0.1:5500",
methods: ["GET", "POST"],
credentials: true
}))
存取私有網路 - RFC1918 CORS
為了防止 cross-site request forgery (CSRF) 透過家中的 router 到達私有或區域網路中的其他設備,Google 提出 CORS for private networks (RFC1918)。其最主要的更動是在 Chrome 94 版之後,無法從不安全的網站(http 就是在說你)發送 request 給私有網路。
而受到影響的存取會有:
- 公開網路對私有網路的存取(Requests from the public network to a private network)
- 公開網路對區域網路的存取(Requests from the public network to a local network)
- 私有網路對區域網路的存取(Requests from a private network to a local network)
那...私有網路存取是什麼
Private network requests are requests whose target server's IP address is more private than that from which the request initiator was fetched. For example, a request from a public website (https://example.com) to a private website (http://router.local), or a request from a private website to localhost.
要搞懂什麼是私有網路存取,就必須先定義什麼是區域、私有及公開網路。
區域 IP 位置(Local IP address space)中包含 IPv4 的 loopback addresses 127.0.0.0/8
及 IPv6 loopback addresses 0:0:0:0:0:0:0:1
(或 ::1/128
)。
私有 IP 位置(Private IP address space)中包含那些僅在該網域或網路區段有意義的 IP 位置,像是 router 的內部網路。例如 10.0.0.0/8
、172.16.0.0/12
及 192.168.0.0/16
。
最後,在這些類別以外的網路均屬於公開網路。
Loopback address 讓一個系統可以傳送訊息給自己的一段 IP address,可用來確認 TCP/IP stack 是否安裝及正常運作,其實就是 localhost
。IPv4 以 127
作為開頭的 IP address 均為 loopback address。
在這三種 IP address 之中,區域網路最為私密,接著是私有網路,最後是公開網路。三者間的關係如下圖所示

這和 CORS 有什麼關係
在這個新改動中加入了兩個 preflight headers,分別是 Access-Control-Request-Private-Network
及 Access-Control-Allow-Private-Network
。
在這個改動之後,任何發送給 private network 的 request 都需要先發送 preflight request,且這個 preflight request 會帶入 Access-Control-Request-Private-Network: true
的 header,無論是何種請求方法或模式。若是 Server 允許這個請求,則需要在 response 中帶入 Access-Control-Allow-Private-Network: true
的 header。
此外,如果較不私密的網路(如私有 IP)想要發送請求給更私密的網路(如區域 IP),那麼即便是在 same-origin
模式下的請求,也必須先發送 preflight request 後才能發送正常請求。
來看點實際範例
在 https://foo.example/index.html
中嵌有一個 image tag <img src="https://bar.example/cat.gif" alt="dancing cat"/>
,其圖片位置的 domain bar.example
會解析至 192.168.1.1
這個 IP 位置,為私有網路中的 IP。因此,這個請求即為「公開網路」發送給「私有網路」的請求。
根據上述提到的新規範,Chrome 會送出長這樣的 preflight request,其中帶有 Access-Control-Request-Private-Network: true
這個新的 header。
HTTP/1.1 OPTIONS /cat.gif
Origin: https://foo.example
Access-Control-Request-Private-Network: true
如果要讓這個 preflight request 能夠成功,那麼 server 必須在 response 中帶入 Access-Control-Allow-Private-Network: true
header。
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Private-Network: true
最後在 preflight request 成功後,Chrome 便會接續發送真正的 request,將圖片資源請求回來。
HTTP/1.1 GET /cat.gif
...
結論
遇到 CORS 怎麼辦 😫
理直氣壯的找後端 (✅)
Reference
- Cross-Origin Resource Sharing (CORS)
- MDN - Preflight request
- Fetch Standard - CORS-preflight cache
- [教學] 深入了解 CORS (跨來源資源共用): 如何正確設定 CORS?
- Feedback wanted: CORS for private networks (RFC1918)
- Private Network Access update: Introducing a deprecation trial
- Private Network Access: introducing preflights