iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Modern Web

重寫一套比我還老的系統:21 歲的校園文字廣播系統系列 第 17 篇

Day 17|CORS?反向代理?這都是些什麼跟什麼啊???

  • 分享至 

  • xImage
  •  

昨天,我們終於把登入 API 串起來了。

但在繼續往下之前,我們先回頭看一眼昨天的程式碼。

return this.http.post<void>(
  '/auth/login',
  request,
  { withCredentials: true }
);

怎麼了?

你沒發現少了什麼嗎?

localhost:8000?

……

你怎麼知道?

廢話,你後端不就跑在 localhost:8000?

也是。

總之,正常來說,如果 Angular 跑在:

http://localhost:4200

FastAPI 跑在:

http://localhost:8000

那前端要直接呼叫後端,第一直覺大概會寫成:

this.http.post(
  'http://localhost:8000/auth/login',
  request
);

但昨天我們沒有。

我們只寫了:

/auth/login

而且它還真的通了。

喔,你還用這招喔。

?

反向代理啊。

???

就之前我第一次碰 Web 架構寫後端的時候,不是剛好也摸到 Nginx 嗎?

後來我發現它可以把 Request 轉發到別的地方。

嗯。

然後我那時候不是因為 CORS 前端的 API 打不過來?

嗯……

所以我就想到一個很邪乎的方法。

……

我好像知道你要講什麼了。

既然跨來源會被 CORS 擋,那我只要讓它看起來沒有跨來源不就好了?

對。

所以我想了一個挺邪門的做法。

我直接用反向代理把 API 掛在同一個 Origin 底下。

瀏覽器打 /api,反向代理再偷偷幫我送去後端。

對。

這樣瀏覽器看到的都是同一個 Origin,CORS 不就直接被我繞過去了?

對阿。

我只能說,這方法其實不怎麼邪門。

甚至可以說——

還挺正規的。

真假?

真的。

不過在解釋這件事之前,我們得先搞清楚幾個問題。

為什麼:

http://localhost:4200

跟:

http://localhost:8000

明明都是 localhost,瀏覽器卻不把它們當成同一個 Origin(來源)?

CORS 到底是在擋什麼?

還有最重要的——

昨天我們明明只寫了:

/auth/login

這個 Request 最後到底是怎麼跑到:

http://localhost:8000/auth/login

去的?

CORS、Origin、反向代理……

這都是些什麼跟什麼啊???

別急。

我們一個一個來。

所以,CORS 到底是什麼?

先從剛才那兩個網址開始吧。

http://localhost:4200
http://localhost:8000

這不就都是 localhost 嗎?

Host(主機)確實都是 localhost。

但瀏覽器判斷兩個網址是不是同一個 Origin(來源)時,看的可不只有 Host。

一個 Origin 基本上由三個東西組成:

Scheme + Host + Port

也就是:

協定 + 主機 + 連接埠

所以把剛才兩個網址拆開來看:

Angular FastAPI
Scheme http http
Host localhost localhost
Port 4200 8000

Scheme 一樣。

Host 也一樣。

但 Port 不一樣。

所以對瀏覽器來說:

http://localhost:4200

跟:

http://localhost:8000

就是兩個不同的 Origin。

就差一個 Port 也不行?

對。

真 scooter。

其實它管這麼多是有原因的。

假設今天你登入了一個網站:

https://bank.example

瀏覽器裡也留著這個網站的登入狀態。

結果你手滑點進一個奇怪的網站:

https://114514.example

如果瀏覽器允許任何網站的 JavaScript 隨便讀取其他 Origin 的資料,那這個奇怪網站就可以試著向:

https://bank.example

發送 Request,再把 Response 裡的資料讀回來。

那事情顯然不太妙。

所以瀏覽器有一套很重要的安全機制:

Same-Origin Policy(同源政策)。

它會限制不同 Origin 之間的互動,尤其是不允許網頁裡的 JavaScript 隨便讀取其他 Origin 的 Response。

等等。

嗯?

那我剛才說「跨來源會被 CORS 擋」是不是怪怪的?

終於發現了。

嚴格來說,真正先限制你的不是 CORS。

而是 Same-Origin Policy。

那 CORS 是來幹嘛的?

來開門的。

?

Same-Origin Policy 預設把不同 Origin 隔開,但現實世界裡,我們當然還是會有正當的跨來源需求。

例如:

https://frontend.example

就是需要呼叫:

https://api.example

總不能因為它們不同 Origin,就一輩子不准講話吧。

所以才有了 CORS:

Cross-Origin Resource Sharing(跨來源資源共享)。

它讓 Server 可以透過 HTTP Response Header 告訴瀏覽器:

「這個 Origin 是我允許的,你可以讓它讀我的 Response。」

例如:

Access-Control-Allow-Origin: https://frontend.example

意思就是告訴瀏覽器:

https://frontend.example

可以存取這個資源。

所以如果要講得更精確一點:

不是 CORS 把跨來源 Request 擋住了。

而是瀏覽器本來就受到 Same-Origin Policy 的限制,而 CORS 提供了一套機制,讓 Server 可以明確允許特定的 Cross-Origin Request(跨來源請求)。

所以 CORS 不是牆?

不是。

它反而比較像牆上的門?

嗯……

這個比喻意外地還行。

Same-Origin Policy 對不同 Origin 之間的互動設下限制,而 CORS 則讓 Server 告訴瀏覽器,哪些跨來源存取是允許的。

而事情到了我們這裡,還會再麻煩一點。

因為還記得昨天這個嗎?

{ withCredentials: true }

記得啊,不就讓瀏覽器把 Cookie 帶上?

對,在我們現在這個情境裡,最重要的就是讓 Cookie 能跟著 Request 一起送。

我們用的偏偏又不是單純的跨來源 API。

我們還有 Session Cookie。

如果真的要讓 Angular 從 localhost:4200 直接呼叫 localhost:8000,除了允許對應的 Origin,還得處理 Credentials(憑證)相關的 CORS 設定。

然後 Cookie 自己還有前幾天講過的那些限制。

HttpOnly、Secure、SameSite……

對。

CSRF……

對。

……

怎麼了?

頭開始痛了。

所以,一年前的你想到了一個方法。

嘿嘿。

先不要嘿嘿。

你當時的想法其實非常單純:

既然麻煩是從「瀏覽器正在跨 Origin 呼叫 API」開始的——

那能不能不要跨?

對啊!

但在回答這個問題之前,我們還差一塊拼圖。

因為你當年解決這件事情,用到了一個叫做 Reverse Proxy(反向代理)的東西。

終於要講我的邪術了?

都說了不是邪術。

我們先從「代理」到底是在代理什麼開始。

所以,代理到底在代理什麼?

其實 Proxy(代理)這個名字聽起來很玄,但概念本身沒有很複雜。

正常情況下,Client(客戶端)要找 Server(伺服器),大概就是:

Client ─────────→ Server

Client 把 Request 直接送給 Server,Server 處理完,再把 Response 回給 Client。

但如果中間多了一層 Proxy:

Client ──→ Proxy ──→ Server

事情就不太一樣了。

Client 不再直接找 Server,而是先把 Request 交給 Proxy,再由 Proxy 幫忙送到真正的 Server。

Server 回傳的 Response,也會先經過 Proxy,再回到 Client。

所以就是找一個中間人?

差不多。

就這?

就這。

Proxy 最核心的概念其實就是:

原本兩邊直接溝通,現在中間多一層幫忙轉送。

當然,實際上的 Proxy 還可以拿來做很多事情。

但我們今天先不挖那麼深。

因為真正重要的問題是——

既然這叫 Proxy,那我們剛才一直講的:

Reverse Proxy(反向代理)

到底「反」在哪?

對欸。

有反向代理,那有正向代理嗎?

有,而且如果是一般使用者平常接觸到「Proxy」這個詞,很多時候指的其實就是 Forward Proxy(正向代理)。

例如我們平常說「設定 Proxy」、「透過 Proxy 上網」,通常就是讓 Client 先把 Request 交給一台 Proxy Server,再由它代替 Client 去存取外面的 Server。

所以它是站在 Client 那邊的?

可以先這樣理解。

Forward Proxy(正向代理),就是站在 Client 這一側,代替 Client 去找 Server 的代理。

假設現在有幾個 Client 想要存取外面的 Server:

Client A ─┐
Client B ─┼──→ Proxy ───→ Server
Client C ─┘

Client 不直接去找 Server,而是讓 Proxy 代替自己去找。

對 Server 來說,直接跟它建立連線、送出 Request 的是 Proxy。

所以它代理的是 Client?

對。

所以如果要用一句很不嚴謹,但很好記的話來講:

Forward Proxy 幫 Client 找 Server。

那 Reverse Proxy 就是……

反過來。

這次 Proxy 不是站在 Client 那一側,而是站在 Server 前面。

                         ┌──→ Server A
Client ───→ Proxy ───────┼──→ Server B
                         └──→ Server C

Client 不需要知道後面到底有幾個 Server,也不一定需要知道真正處理 Request 的 Server 在哪裡。

它只要把 Request 送到 Reverse Proxy。

至於這個 Request 最後應該交給誰——

Reverse Proxy 自己決定。

所以……

Forward Proxy 幫 Client 找 Server。

Reverse Proxy 幫 Server 接 Client。

喔——

所以我那時候就是把後端藏在反向代理後面?

對。

而且講到這裡,我們終於可以回來處理昨天消失的:

localhost:8000

了。

所以,localhost:8000 到底去哪了?

昨天我們寫的是:

this.http.post(
  '/auth/login',
  request
);

這裡的:

/auth/login

沒有寫 Scheme、Host,也沒有 Port。

所以瀏覽器當然沒有什麼神奇的能力,可以自己猜出:

嗯,作者一定是想送去 localhost:8000。

所以如果我們現在開著:

http://localhost:4200

那瀏覽器看到:

/auth/login

實際上就會把它當成:

http://localhost:4200/auth/login

等等。

嗯?

那這不是 Angular 自己嗎?

沒錯。

那 FastAPI 是怎麼收到的?

終於問到重點了。

我們其實在 Angular 的開發伺服器上設定了一層 Proxy。

當它收到符合條件的 Request 時,不是自己處理,而是幫我們把 Request 轉送到:

http://localhost:8000

所以昨天真正發生的事情,其實是:

Browser
   │
   │ POST /auth/login
   ▼
http://localhost:4200
Angular Dev Server
   │
   │ Proxy
   ▼
http://localhost:8000
FastAPI

所以瀏覽器根本不知道 localhost:8000?

至少在這次 API Request 裡,它不需要知道。

瀏覽器只知道:

http://localhost:4200/auth/login

至於 Angular Dev Server 收到之後,又把 Request 轉送到哪裡,那已經是瀏覽器外面的事情了。

而這就產生了一個很有意思的結果。

還記得剛才為什麼會有 CORS 問題嗎?

因為 localhost:4200 跟 localhost:8000 是不同 Origin。

對。

那現在瀏覽器實際送 Request 的地方是哪裡?

localhost:4200。

目前網頁在哪?

也是 localhost:4200。

那有跨 Origin 嗎?

……

沒有。

對。

從瀏覽器的角度來看:

http://localhost:4200
        ↓
http://localhost:4200/auth/login

從頭到尾都是同一個 Origin。

所以這次 Request 根本不需要 CORS 來允許跨來源讀取。

所以我說的沒錯啊。

哪句?

我真的把 CORS 繞過去了。

……

嚴格來說,不是。

蛤?

你沒有把 CORS 關掉,也沒有找到什麼漏洞,更沒有突破瀏覽器的 Same-Origin Policy。

你只是做了一件更單純的事情:

讓瀏覽器根本不需要發出 Cross-Origin Request。

……

這不是更邪乎嗎?

不是。

而且恰恰相反。

這其實是整個做法裡,最棒的一點。

因為我們不是想辦法增加更多設定,讓一個更複雜的架構可以正常運作。

而是直接讓原本需要處理的問題——

不存在。

能不設定,就不要設定

等等,所以你的意思是,只要我不用 CORS,就比較安全?

也不能這樣說。

CORS 本身沒有比較危險,反向代理也不會憑空幫我們套上一層安全 Buff。

真正重要的是——

我們到底需不需要跨 Origin 這件事情。

如果今天真的有這個需求,那就應該好好設定 CORS。

哪些 Origin 可以存取?

能不能攜帶 Credentials?

允許哪些 Method?

允許哪些 Header?

這些東西都可以設定,也都有各自存在的理由。

但問題是:

我們現在根本沒有這個需求。

所以?

那為什麼要為了一個不存在的需求,多留一堆可以設定錯的東西?

……

還記得 Day 9 嗎?

不記得。

……

好啦,Cookie 那篇嘛。

對。

我們那時候花了一整篇在研究怎麼保護 Cookie。

HttpOnly,避免 JavaScript 直接讀取 Cookie。

Secure,要求 Cookie 只透過 HTTPS 傳送。

還有 SameSite,控制 Cookie 在不同站點情境下要不要被送出去。

當時我們其實碰到了一個很現實的問題:

設定越有彈性,我們要做的決定就越多。

如果前端和 API 真的需要跨來源運作,我們就得決定 CORS 要允許哪些 Origin、Credentials 要不要開,以及前端到底該怎麼呼叫 API。

如果系統還需要跨站攜帶 Cookie,那 Cookie 本身的策略又得跟著這些需求一起考慮。

每一個選項都有它存在的理由。

但每多一個選項——

也就多一個選錯的機會。

所以你想全部關掉?

不是。

是只開我們需要的東西。

這兩句看起來很像,意思卻完全不一樣。

我們不是因為不知道 CORS 怎麼設定,所以乾脆逃避 CORS。

也不是因為 SameSite 很麻煩,就把 Cookie 隨便設一設。

而是先回頭問:

這個系統真的需要跨來源嗎?

如果答案是否定的,那我們就不需要為了跨來源,額外放寬原本可以維持的限制。

這也讓 Day 9 那些原本看起來很煩的設定,突然簡單了不少。

所以你繞這麼大一圈,最後的結論是……

能不開的,就不要開。

能不用的,就不要用。

能讓架構本身消除掉的例外,就不要再多加一堆設定去處理它。

聽起來好像很廢話。

確實很像。

但這種廢話,往往是等到設定炸過幾次之後才會開始覺得有道理。

畢竟設定本身不可怕。

可怕的是:

這個要 true 還是 false?

這個 Origin 要不要加?

Credentials 要不要開?

這個 Header 能不能過?

Cookie 這裡要選哪個?

開發環境可以,正式環境怎麼炸了?

當選項越來越多,它們之間又開始互相影響時,我們真正需要維護的就不只是一份設定檔。

而是一堆:

「如果這個開了,那另外那個就要怎樣。」

的組合。

所以少一個選項……

就少一個選錯的機會。

這才是我現在回頭看你那個「邪門方法」,覺得它其實一點都不邪門的原因。

你不是找到了一個方法,把瀏覽器的安全機制繞掉。

你只是無意間做了一件很合理的工程決策:

把不需要存在的複雜度拿掉。

所以一年前的我其實很強?

ummm……一年前的你蠻廢的。

是一年前的我還蠻不錯的。

謝謝謝謝謝謝。

不客氣不客氣不客氣。

總之,回到昨天的:

this.http.post(
  '/auth/login',
  request
);

現在我們終於知道,這裡不是少寫了:

http://localhost:8000

而是我們刻意讓前端根本不需要知道它。

瀏覽器只管把 /auth/login 送到自己認識的 Origin。

至於真正的 FastAPI 躲在哪裡——

交給 Proxy 處理就好了。

而我們也因此少掉了一整組原本不需要存在的跨來源設定。

有時候,讓系統更不容易出錯的方法,不是把所有設定都背得滾瓜爛熟。

而是從一開始,就少給自己幾個可以選錯的選項。


上一篇
Day 16|畫完登入頁,然後呢?該真的讓它登入了吧
下一篇
Day 18|即時通負責溝通,那廣播負責什麼?
系列文
重寫一套比我還老的系統:21 歲的校園文字廣播系統 共 19 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言