iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

Day 7 我們把身分和追蹤碼這兩件「隨身行李」從執行緒身上挪下來,改掛在請求本身(request context)上,讓它們跟著資料流走、跨幾條執行緒都不掉。但那篇從頭到尾有個詞被我反覆用、卻一直沒交代清楚:「驗章還原出來的身分」。我們一直說「把使用者塞進 context」,但這個身分到底是什麼時候、用什麼方式變得可信的?它憑什麼能被信任到後面拿去寫進法遵稽核?今天就往回追到一切的源頭——一個請求的身分,其實早在使用者登入那一刻就確立了,不是等他送出問題才認定。

本篇結構:

  • 「使用者是誰」要在哪一刻確立
  • 用一張簽章 cookie,把身分變成可驗證的事實
  • 不存 session 換來什麼、又賭上什麼

「使用者是誰」要在哪一刻確立

這聽起來像廢話,但它其實是一個有後果的設計選擇。

想像另一種做法:行員小林送出一段話

我的員編是 123456,AD 帳號被鎖了,順便想查內湖分行的營業時間。

系統從這句話裡把「123456」抓出來當成他的身分。這做法有兩個致命問題:

  1. 輸入是使用者自己打的字,他想填誰的員編就填誰的,等於把身分認定權交到了攻擊者手上。
  2. 就算他誠實,員編也只是對話內容的一部分,跟「他這個 session 是誰登入的」是兩回事——你沒辦法靠一段聊天內容去驗證背後那個人。

所以正確的順序是反過來的:身分必須在進入對話之前就確立,而且來源要可信、可驗證。對話裡出現的任何員編、姓名,都只是「他打的字」,不是「他是誰」。這條界線今天劃清楚,後面好幾天都會用到——尤其 Day 24 寫稽核紀錄(audit log)時,那筆紀錄裡的 userId 取自登入時確立的身分,而不是從本次輸入擷取,伏筆就埋在這裡。

那麼登入這一刻發生了什麼?Portal 自己不做帳號密碼、二次驗證這些事,它把登入委託給企業身分中心(IdP)——也就是公司統一的 SSO(single sign-on,單一登入)機制。小林在身分中心通過驗證後,回來時 Portal 要解決一個延伸問題:接下來每一個請求,怎麼知道「這就是剛剛那個登入過的人」,又不必每來一個請求就回頭問身分中心一次?

先把答案的形狀畫出來,機制接著拆:

https://ithelp.ithome.com.tw/upload/images/20260809/20183385Nl4e6VT3Tv.png

用一張簽章 cookie,把身分變成可驗證的事實

最直覺的答案是「在伺服器存一張 session 表」:登入成功就在記憶體(或外部快取)裡記一筆,發個 session id 給瀏覽器,之後每個請求拿 session id 來查表。但 Day 4 鋪過 Portal 的選型基調——它刻意做成無狀態(stateless),自己不存資料庫、也不想維護 session 狀態。一座要橫向擴展、隨時可以多開幾個實例的閘道,最不想要的就是「身分狀態黏在某一台機器上」。

Portal 的做法是把身分本身直接交給瀏覽器保管,但加上防竄改的封條。登入成功後,伺服器發一張帶簽章的身分 cookie,名字叫 EMP_INFO,結構大致是這樣:

EMP_INFO = base64url( payload )
         + "."
         + base64url( HMAC_SHA256(payload, serverSecret) )

# payload 是身分內容本身,大致長這樣:
{
  "corpId": "12345678",     # 8 碼集團員工編號,身分的唯一識別
  "name":   "林**",
  "roles":  ["EMP"],
  "iat":    1718960103      # 簽發時間
}

點號前半段是身分內容本身,用 base64url 編碼——注意,這只是編碼、不是加密,任何人都能解開來看。後半段才是關鍵:用伺服器才握有的金鑰 serverSecret,對 payload 算出來的一段 HMAC(雜湊訊息驗證碼)。

驗章的邏輯很單純,每個請求進門時做一次:

  1. 切出 cookie 的兩段:payloadPartsignaturePart
  2. 用伺服器自己的 serverSecret,對 payloadPart 重算一次 HMAC_SHA256
  3. 把重算出來的結果,跟 cookie 帶來的 signaturePart 比對(常數時間比對):
    • 一致 → 信任 payload,解出 corpId / roles,當成本次請求的身分。
    • 不一致 → 視為未登入(cookie 被竄改或偽造)。
  4. 把還原出的身分塞進 Day 7 講的 request context,一路跟著請求往下傳。

關鍵在第 2、3 步:serverSecret 只有伺服器有。攻擊者就算看得懂前半段、把 corpId 從自己的 12345678 改成同事的,想冒名查別人的資料——他改得動 payload,卻算不出對應的新 HMAC,因為他沒有金鑰。改了內容,簽章卻配不上,第 3 步一比對就破功,整張 cookie 作廢、當成沒登入。比對本身刻意用常數時間(constant-time)做,而不是一發現某個位元不同就提前回傳——避免有人靠回應的快慢差異,一個位元一個位元地把正確簽章猜出來(timing attack)。

這就是為什麼伺服器不需要存任何 session 表也能信任這張 cookie:它信任的不是「我記得發過這張票」,而是「這段簽章只有我自己算得出來,既然對得上,內容就沒被動過」。身分因此成了一個自帶證明的事實,而不是一筆要回頭查詢的紀錄。

也正因為它自帶證明、不依賴伺服器狀態,Day 7 講的「把身分放進 request context 一路傳下去」才成立:驗章是在請求進門的那一道 filter 一次做完的,往後整條 reactive 鏈上傳遞的就是這個已驗證、可信的身分,下游不必再回頭碰 cookie、也不必查任何表。Day 7 是「身分怎麼往下傳」,Day 8 是「身分一開始怎麼變可信」,兩篇接在同一個點上。

順帶補一條界線:cookie 驗不過時一律當作未登入,而不是直接回錯誤把請求擋死——提問這條路徑遇到身分問題傾向 fail-open(放行、標匿名),不讓身分中心偶爾不穩就拖垮整個助理。這個「該放行還是該擋」的容錯選擇,Day 10 會專門講透,這裡先按下不表。

不存 session 換來什麼、又賭上什麼

任何設計都是有代價的,把這筆帳算清楚才算誠實。先把 stateless 簽章 cookie 與傳統 server-side session 兩種路線並排看:

面向 server-side session stateless 簽章 cookie(Portal 採用)
身分存放處 伺服器端的 session 表(記憶體或外部快取) 瀏覽器手上的 EMP_INFO cookie
驗證一個請求 拿 session id 跨網路查中央 session store 一次本地的 HMAC_SHA256 運算
橫向擴展 需要 session 黏著(sticky session)或跨機器同步狀態 零狀態,請求落到哪一台都能獨立驗章
單點故障 共享的 session 儲存是一個故障源 無中央 session store,少一處故障點
撤銷某個身分 可在伺服器端撤銷某張 session 沒有這個後門,金鑰是唯一要害

不存 session 的好處很實在,上表右欄就是紅利的全貌:伺服器零狀態,想多開幾台實例分流,請求落到哪一台都能獨立驗章;驗證純粹是一次本地運算,不引入額外的故障點。對一座定位是「對外閘道、要扛高並行」的 Portal 來說,這跟它整體的 reactive、無持久化(persistence)路線是同一組選擇——少一個有狀態的元件,就少一處要顧的一致性與擴展瓶頸。

但代價也很尖銳,而且就壓在那把 serverSecret 上。

第一,金鑰外洩等於信任體系崩潰。 整套身分的可信度,全部押在「只有伺服器算得出 HMAC」這個假設上。一旦 serverSecret 洩漏,攻擊者就能自己捏造任意 payload、算出合法簽章,偽造出「我是任何人」的 cookie——而且因為伺服器不存 session、無從比對「我到底發過哪些票」,這種偽造幾乎無痕。這跟 session 模式有本質差異:session 模式下你還能在伺服器端「撤銷某張 session」,stateless 簽章卻沒有這個後門,金鑰就是唯一的要害。所以這把金鑰的保管、輪替(rotation)、外洩後的應變,是這個架構必須認真對待的功課,不是發完 cookie 就沒事了。

第二,簽章不解決「過期」。 HMAC 只證明「這段內容沒被改過」,不證明「這段內容還沒過期」。如果只簽身分內容、不帶任何時效資訊,那一張 cookie 在金鑰沒換之前理論上可以一直有效。所以 payload 裡才會帶上 iat 這類簽發時間,驗章通過之後再多檢查一次時效——簽章保「沒被竄改」,時效欄位保「還在有效期內」,兩件事要分開做。

第三,內容是攤開的。 前面說過 payload 只是 base64url 編碼、不是加密,任何拿到 cookie 的人都能看到 corpId、姓名、角色。所以這張 cookie 不該裝任何機密——它的設計目的是「攜帶可驗證的身分聲明」,不是「藏祕密」。它真正防的是「竄改」(靠簽章),而不是「被看見」。

把這三點放在一起看,stateless 簽章 cookie 是一個典型的工程取捨:用「賭一把金鑰能守住」換來「整個身分層零狀態、好擴展」。對 Portal 這種高並行、要無狀態水平擴展的閘道,這筆交易划得來——前提是金鑰真的被當成最高機密在守,而不是當成發完就不用照看的東西。

總結:身分在登入那一刻就由 SSO 確立,往後每個請求靠驗章重新確認,而不是重新登入;對話內容裡的員編姓名,永遠只是「他打的字」,不是「他是誰」。 這條界線會一路撐到 Day 24 的稽核。

明天 Day 9,身分確立、請求正式進門,我們來談進門時做的第一件不起眼卻能救命的雜事——發一組 correlation id,讓這次請求散落各處的足跡,事後都能用同一個編號重新拼回一條線。


上一篇
Day 7|reactive 下的上下文傳遞
下一篇
Day 9|是誰在那裡??
系列文
轉生到全端工程師沒多久就要負責公司的大平台??9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言