從遊戲來認識 CORS 與瀏覽器的限制

很重要的觀念哦!

前言

這篇文章主要會拿 Lidemy HTTP Challenge 這個小遊戲來做說明,所以建議玩過之後再來看,不然可能會看不懂。

另外我也寫了一篇攻略文:HTTP Challenge 攻略與心得 ,有興趣可以參考看看。

關於 CROS

CORS 的全名為「Cross-Origin Resource Sharing(跨來源資源共用)」,是一個「瀏覽器」的規範,目的是要讓我們可以「到不同來源的地方」去拿資料。

所以在解釋 CROS 之前,你要先了解為什麼需要 CORS?沒有 CORS 會有什麼問題?

沒有 CORS 會有什麼問題?

故事一樣要從瀏覽器說起,首先有一個叫做「同源政策(Same-origin policy)」的規範,內容是如果「請求資料方」跟「提供資料方」兩者不同源,瀏覽器就會把 response 給擋下來,至於什麼是「不同源」等一下會在解釋。

首先大部分的人在玩 Lidemy HTTP Challenge 的時候是透過 curl 或是在 Node.js 裡搭配 request 來玩的,所以不會碰到這個問題。

但是如果你改用「瀏覽器」的話就不一樣了,拿第三關來舉例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
備註:這一關要新增一本書到圖書系統裡
*/
const xhr = new XMLHttpRequest()
// 要傳送的資料
const data = new FormData()
data.append('name', '《大腦喜歡這樣學》')
data.append('ISBN', '9789863594475')
// request 資訊
xhr.open('POST', 'https://lidemy-http-challenge.herokuapp.com/api/books')
// 送出 request
xhr.send(data)
// 拿到回應
xhr.onload = function () {
console.log(this.responseText)
}

這時候打開 console 會看到:

cors-1

備註:這裡我有開 liver-server,所以網址才會是 http://127.0.0.1:5500

這個就叫做「不同源」,因為「域名」不一樣:

  • 請求資料者(我) http://127.0.0.1:5500 的域名是 localhost(這個 ip 就是對應到 localhost)
  • 資料提供者(Lidemy)的域名是 lidemy-http-challenge.herokuapp.com

(嚴謹一點來說的話 httphttps 也是不同源,不同的 port 也是不同源,只是最常見的是域名)

這兩個是不一樣的,所以雖然 request 被送出去了,但回傳結果被瀏覽器給擋下了。

再特別強調一次,request 還是會送出去,只是結果會被瀏覽器擋下來。

切換到 Network 的欄位,可以看到以下資訊:

cors-2

Status Code 是 200,代表處理成功。也就是說你其實是有新增書本到 server 端,只是回傳結果被瀏覽器擋下來了而已。

備註:其實像 POST 這種會去改 server 端資料的 request 通常會用「Preflight Request」比較嚴謹(詳細可以參考 輕鬆理解 Ajax 與跨來源 request ),不然如果哪天我隨便發一個 DELETE 到 server 就可以直接把東西給刪除掉,也太不安全了,對吧?

cors-3

從這裡可以看到雙方的來源,Host 是 Lidemy 的位置,Origin 是我的位置。

所以只要這種 HostOrigin 是來自不同地方的 request ,瀏覽器都會把 response 給擋下來。

為什麼需要 CORS?

所以在 同源政策(Same-origin policy) 的規範下,根本不可能做到「去別的地方拿資料」這件事。

CORS 就是用來解決這個問題的。

還記得前面剛剛說過 CORS 的用意是讓我們「到不同來源的地方去拿資料」這件事嗎?現在你只要在 server 端加上一個 header:access-control-allow-origin,就可以做到這件事情。

access-control-allow-origin 是 CORS 這個規範下的一個 header,用來讓 sever 決定「誰可以存取這個資源」的意思。

如果我只想讓「http://example.com:8080」來存取資源,server 就設定:

access-control-allow-origin: http://example.com:8080

如果要讓「任何人」都能存取,那就用萬用字元「*」來設定:

access-control-allow-origin: *

以 Twitch 的 API 來舉例:

cors-4

可以跟 Lidemy 對照一下:

cors-5

兩個只差在有沒有 access-control-allow-origin 而已,Lidemy 沒有加上這個 header,所以我們發 request 的時候 response 會被瀏覽器給擋住,但是 Twitch 有,所以發 request 給 Twitch 不會被瀏覽器給擋下 response:

cors-6

雖然已經說過很多次了,但還是要再強調一次。你一定要搞清楚瀏覽器擋下的是 sever 回傳的 response,不是你發出去的 request,這個差異很重要。

最後統整幾個重點:

  1. 因為同源政策的關係,所以沒辦法到別的地方拿資料
  2. 同源政策的機制是瀏覽器會把 response 擋下來,但實際上 request 還是有發出去
  3. CORS 是用來解決同源政策的限制,實現「跨來源」交換資料這件事,實現的方法就是在 server 端加上 access-control-allow-origin: *

大致上就是這樣,不過還有另外一種實現「跨來源」交換資料的老方法,叫做「JSONP」,簡單來說就是藉由 <script src="xxx"> 來發出 request ,詳細可以參考我以前寫的文章:實作 JSONP

關於瀏覽器的限制

瀏覽器除了有同源政策的規範之外,還有一個限制是不可以竄改 User-Agent 的內容。

在遊戲的第九關,不是會要你去把 User-Agent 改成 IE6 的內容嗎?如果用瀏覽器來做的話一樣會發現行不通:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
備註:這一關要取得「系統資訊」中的資料
*/
const xhr = new XMLHttpRequest()
// 設定request
xhr.open('GET', 'https://lidemy-http-challenge.herokuapp.com/api/v2/sys_info')
// http basic 驗證
xhr.setRequestHeader('Authorization', 'Basic YWRtaW46YWRtaW4xMjM=')
// 題目要求的自定義 header
xhr.setRequestHeader('X-Library-Number', '20')
// 把 user-agent 改成 IE6 的資訊
xhr.setRequestHeader('user-agent', 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)')
// 送出request
xhr.send()
// 拿到回傳結果
xhr.onload = function () {
console.log(this.responseText)
}

結果如下:

cors-7

除了不同源的問題之外,現在還蹦出了一個 Refused to set unsafe header "user-agent" 錯誤,簡單來說就是「不要亂改 user-agent,這樣會初四啦」。

這時候你檢查一下 request header,就能看到真的沒有被改掉:

cors-8

但如果是在「不是瀏覽器」的情況下(例如 Node.js),就不會有這個限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
備註:一樣是第九關的內容
*/
const API_ENDPOINT_V2 = 'https://lidemy-http-challenge.herokuapp.com/api/v2'
request(
{
method: 'GET',
url: `${API_ENDPOINT_V2}/sys_info`,
headers: {
Authorization: 'Basic YWRtaW46YWRtaW4xMjM=',
'X-Library-Number': '20',
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)'
}
},
(err, res, body) => {
// 印出回傳的狀態碼
console.log(res.statusCode)
// 印出 header 的資訊
console.log(res.request.headers)
}
)

結果:

1
2
3
4
5
6
200
{
'Authorization': 'Basic YWRtaW46YWRtaW4xMjM=',
'X-Library-Number': '20',
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)'
}

所以看完這篇文章後一定要搞清楚 是不是在瀏覽器執行的? 會影響最後的結果。

如果是,那你就要注意瀏覽器會有哪些限制,像剛剛介紹的「同源策略」和「不可以竄改 User-Agent」等等。

不是的話呢?那恭喜你,以上提到的限制全部都不會發生。

文末

其實在玩遊戲的時候沒有考慮到那麼多事情,是因為看了 幕後花絮:Lidemy HTTP Challenge 的設計以及彩蛋 後才發現這些藏在裡頭的眉眉角角。

所以如果你原本跟我一樣沒有發現這些細節的話,希望這篇文章能夠為你帶來幫助。

mentor-program-day29 HTTP Challenge 攻略與心得
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×