網頁架構與資安演義:從單體到分離,一場銀行櫃檯的革命

這是一個關於「銀行櫃檯(伺服器)」、「大廳經理(CDN)」與「客戶(瀏覽器)」之間,為了效率與安全而不斷過招的故事。

這是一個關於「銀行櫃檯(伺服器)」、「大廳經理(CDN)」與「客戶(瀏覽器)」之間,為了效率與安全而不斷過招的故事。


第一章:傳統銀行時代(單體架構 Monolith)

在很久以前,網站就像一間傳統銀行。這裡只有一個核心角色:萬能櫃員(PHP Server)

當客戶(使用者)走進銀行要看商品目錄時:

  1. 客戶走到櫃檯。
  2. 櫃員轉身去倉庫(資料庫)搬出厚厚的商品資料。
  3. 櫃員拿出紙筆,現場畫出一張漂亮的表格,填上數據,貼上照片,裝訂成一本精美的型錄(HTML)。
  4. 最後雙手遞給客戶。

問題來了:生意太好,每天有一萬個客戶來要型錄。櫃員每天都在做重複的工:「去倉庫、畫表格、填數據」。櫃員累垮了(CPU 飆高),後面的客戶大排長龍。


第二章:分工革命(前後端分離 & CDN)

銀行決定改革。他們聘請了一位大廳經理(CDN / 前端靜態資源),並制定了新規則:「殼肉分離」

  1. 殼(Shell):表格的格式、Logo、裝潢、按鈕。這些是不變的。
  2. 肉(Data):帳戶餘額、最新的商品庫存。這是會變的。

現在流程變了:

  1. 大廳經理(CDN) 站在門口。手上抱著一萬份已經印好的空白表格(index.html + JS + CSS)。
  2. 客戶一進門,經理直接塞給他一份空白表格:「拿去,自己填。」
  3. 客戶找個位子坐下,表格上有個神奇的小精靈(JavaScript),小精靈會跑去櫃檯問櫃員:「欸,給我最新的數據!」
  4. 櫃員現在輕鬆多了,他不用畫表格,只要講一句話:「餘額 500 元」(JSON)。
  5. 小精靈收到數據,瞬間把數字填進表格裡。

這就是「API 快取」的威力: 對於像「商品清單」這種大家看到的都一樣的數據,櫃員甚至可以在櫃檯放個錄音機Cache-Control Header)。 當小精靈來問數據時,如果上一分鐘才有人問過,錄音機就直接播放:「洗髮精 200 元」,櫃員連頭都不用抬。


第三章:隱形的識別證(Session ID & HttpOnly)

為了辨識客戶身分,銀行發明了一種 「縫在衣服裡的識別證」(Session ID Cookie)

當客戶第一次驗證身分(登入)成功後,櫃員會拿出針線,把一張寫著代碼的識別證,死死地縫在客戶的外套內側口袋裡

這個動作有兩個特點:

  1. 自動出示:客戶以後只要靠近櫃檯(發送 Request),不需要自己掏證件,外套會感應並自動向櫃員送出訊號證明身分(瀏覽器自動夾帶 Cookie)。
  2. 防偷窺(HttpOnly):這張證件縫得太裡面了,連客戶自己(前端 JavaScript)都翻不出來看,當然,躲在客戶旁邊想偷看的小偷(XSS 惡意腳本)也看不到。

這就是為什麼在前後端分離時,前端工程師常說「我拿不到 Session ID」,因為你根本不需要拿,瀏覽器這個外套會幫你搞定一切。


第四章:蒙面大盜的陰謀(CSRF 攻擊)

有一天,出現了一種狡猾的攻擊手法:「借刀殺人」(CSRF)

壞人(惡意網站)不能直接偷你的識別證(因為縫在口袋裡 HttpOnly 拿不到),但他發現了一個漏洞:「反正只要你靠近櫃檯,外套就會自動亮出識別證」

於是,壞人在銀行對面開了一家「免費抽獎店」。當你走進去(訪問惡意網站),壞人誘導你填寫一張單子,這張單子其實是**「轉帳 100 萬給壞人」的匯款單**,但上面蓋了一層假抽獎券。

壞人把你推向銀行的櫃檯。

  1. 你靠近櫃檯(瀏覽器發送 POST 請求)。
  2. 你的外套很盡責,感應到櫃檯,立刻亮出 VIP 識別證(Cookie 自動帶上)。
  3. 櫃員看到識別證:「喔,是 VIP 客戶,身分確認。」
  4. 櫃員收下那張被偽裝的匯款單,執行轉帳。

你就在不知不覺中,被自己的外套出賣了。這就是 CSRF(跨站請求偽造)。


第五章:號碼牌制度(CSRF Token)

為了防堵這個漏洞,銀行經理推出新規定:「辦事不只要看臉,還要看號碼牌」

這個**號碼牌(CSRF Token)**有幾個特性:

  1. 隨機產生:每次都不一樣,猜不到。
  2. 無法偽造:只有真的走進銀行大廳(訪問真的網站)的人,經理才會發給你。
  3. 必須手持:這張牌子不縫在衣服裡,你要自己拿著遞給櫃員。

現在,壞人的計謀失效了。 當壞人把你推向櫃檯時,你的外套雖然還是亮出了識別證(Session OK),但櫃員伸手一討:「你的號碼牌呢?」 壞人拿不到銀行內部的號碼牌,所以你手上空空如也。 櫃員臉色一沉:「CSRF Token Mismatch,警衛!」


第六章:影分身之術的災難(多分頁與 Token 策略)

這套號碼牌制度實施後,大家都很滿意,直到有一天,來了一位會用「影分身之術」的客戶(開啟多分頁的使用者)。

災難現場:用完即丟的號碼牌(Per-Request Token)

起初,銀行採用**「極端安全」**策略:每張號碼牌只能用一次,用完就作廢換新的。

  1. 本尊(分頁 A) 進場,拿了號碼牌 123
  2. 分身(分頁 B) 突然也被變出來(右鍵開啟新分頁),他也拿了號碼牌 123
  3. 分身(分頁 B) 跑去櫃檯按了個讚(送出請求)。櫃員收走 123,發給他一張新的 456
    • 此時銀行系統紀錄:現在有效的號碼牌是 456
  4. 本尊(分頁 A) 慢吞吞地要結帳,遞出了手上的 123
  5. 櫃員大怒:「這張 123 早就過期了!現在是 456!」
  6. 本尊被轟出銀行(頁面錯誤 419)

解決之道:無限暢飲的號碼牌(Per-Session Token)

銀行發現這樣不行,沒人能好好辦事,於是改為**「跟隨身分」**策略:

「只要你今天還沒離開銀行(沒登出),你手上的號碼牌『一直是同一張』。」

  1. 本尊 進場,拿到號碼牌 XYZ
  2. 分身 被變出來,也拿到號碼牌 XYZ
  3. 不管本尊還是分身,只要去櫃檯,拿的都是 XYZ。櫃員只檢查你的臉(Session)對不對,以及號碼牌是不是 XYZ,完全不管你用了幾次。

這就是現代網站的運作方式:不用擔心開分頁,因為大家共用同一個 Session,也就共用同一張 CSRF 號碼牌。


第七章:新時代的難題與解法(SPA 與 Cookie-to-Header)

在前後端分離的時代(現代銀行),這裡有個小尷尬。

傳統時代,號碼牌是直接印在申請單(HTML form hidden input)上的。但現在申請單是大廳經理(CDN)發的空白表格,上面沒有印號碼牌,號碼牌要怎麼發?

可以選擇 Double Submit Cookie 策略:

  1. 發放:當你登入時,櫃員除了縫識別證(Session ID, HttpOnly),還會給你另一張貼紙貼在手背上,寫著號碼(XSRF-TOKEN Cookie)。這張貼紙沒有縫死(HttpOnly = false),你可以自己看。
  2. 抄寫:當你要辦事時,表格上的小精靈(JS)會看一眼手背上的貼紙,把號碼抄在申請單的標頭欄位(HTTP Header: X-XSRF-TOKEN)上。
  3. 驗證:櫃員收到單子,檢查兩件事:
    • 識別證對不對?(Session ID, HttpOnly)
    • 手背貼紙的號碼,跟單子上抄寫的號碼,是不是同一個?

為什麼這樣防得住壞人? 因為壞人的網站(不同的 Domain)受限於「同源政策」,他既看不到你手背上的貼紙(讀不到 Cookie),也不能叫你的瀏覽器把貼紙內容抄到 Header 裡。


結語

從單體到分離,從 Cookie 到 Token,這一切演進都是為了解決兩個矛盾:

  1. 怎麼讓好人更方便?(CDN 快取、自動帶 Session)
  2. 怎麼讓壞人更難過?(HttpOnly 防 XSS、Token 防 CSRF)

現在,當你在寫 fetch 加上 credentials: 'include' 時,你其實是在告訴瀏覽器:「嘿,啟動那件神奇外套的功能吧!」而當你處理 CSRF Token 時,你則是在訓練那隻幫你抄寫號碼牌的小精靈。