Day 7:登入 Token 設計:從「幾小時自動登出」問下去,發現整套驗證都沒啟用
一個沒設的環境變數 = header 即身分;拆解 token 的六個參數,以及少一個會怎樣
問的是「幾小時自動登出」,查出來的是「根本沒有在驗身分」。
問的是「幾小時自動登出」,查出來的是「根本沒有在驗身分」。
Token 機制寫得很完整——簽章、有效期、自動續期、版本撤銷全都有。 唯一缺的是一個環境變數沒設,所以整道驗證一直在「觀察模式」:記 log,然後放行。
這篇拆解那套 token 設計動用了哪些參數、每一個少了會怎樣。 一、從「登入逾時」問下去
要回答「多久自動登出」,得先找到 TTL。搜出來是這樣:
看起來答案就是「12 小時」。但常數存在不等於它在跑——再往下一層,驗證那道 gate 寫著:
去環境變數裡找 SESSIONTOKENENFORCE:不存在。
到這裡都還只是讀程式碼的推論。發一個「應該要被擋」的請求才算數:
沒帶任何憑證。所以真正的答案是:「12 小時過期」沒有任何效果,因為系統根本不要求 token。 判準:驗「安全設定有沒有生效」,永遠是發一個應該被拒絕的請求看回應碼, 不是讀設定檔、也不是找常數。設定檔會騙人,401 不會。 二、沒有那個變數,等於什麼都沒有
身分判斷只看兩個 HTTP header:
而工號不是秘密——內部分機表上有上千筆工號對姓名,名牌上有,email 簽名檔也有。
更麻煩的是管理員判定用同一組 header:
知道任何一位管理員的工號,就能用管理員身分呼叫後台 API。這不是「懶得登出」等級的問題, 是身分完全可以自稱。
沒設變數(現況) 設了之後 --- --- --- 身分怎麼證明 宣稱即可 出示 HMAC 簽章的 token,沒有密鑰偽造不出來 拿到別人工號能做什麼 讀他的資料、代他操作 什麼都做不了 冒充管理員 可以 不行 閒置超過 TTL 永遠有效 自動登出 舊 session 想撤銷 無從撤起 版本一升,全部失效
原本問的「自動登出」只是最後兩列。 三、這套 token 動用到的六個參數
這是全篇重點。任何一個少設,整套的保護就打折或靜默失效。 ① AITOOLSIGNINGSECRET — 簽章密鑰
token 格式是 <base64url(payload)>.<hex-hmac>,payload 內容:
payload 是明文可讀的(base64url 不是加密)。安全性來自「改了內容 hmac 就對不上」, 不是來自「看不到」。所以 payload 裡不要放機密,放的是「這個人是誰、到什麼時候為止」。
沒設會怎樣:簽不出 token。這時系統通常設計成「不擋登入」——於是你以為有 token, 其實全部走 fallback。這是最容易誤判成「有在跑」的狀態。 ② SESSIONTOKENENFORCE — 從觀察轉成強制
前面整段的主角。沒設 = 前五個參數全部白做。
設計成兩段式是對的:先上線只記 log,觀察一陣子確認沒把人鎖在外面,再打開。 問題是「觀察期」很容易變成永久——沒有人回頭按那個開關,而且系統看起來一切正常。 教訓:任何「先觀察、之後再開」的安全開關,上線那天就要排定關掉觀察模式的日期, 否則它會一直停在觀察模式,而且沒有任何症狀提醒你。 ③ DEFAULTTTLSEC — 有效期
員工 12 小時、超管 24 小時。這是「閒置多久自動登出」的實際答案。
取捨:太短使用者每天重登很煩;太長則 token 外洩後的有效視窗變長。 配合下一個參數,12 小時對「上班用」是合理的——當天用就一直續,隔天早上才需要重登。 ④ REFRESHTHRESHOLDRATIO — 滾動續期門檻
這個參數決定體感。有它,活躍使用者永遠不會被登出(每次請求都在續); 沒它,所有人每 12 小時被踢一次,不管正在做什麼。
「自動登出」要能被接受,靠的不是 TTL 短,而是續期讓 TTL 只對真正閒置的人生效。 ⑤ CORS exposedHeaders — 續期的隱形前提
換發的新 token 是透過 response header 回傳的:
而瀏覽器 JS 預設讀不到自訂 response header,除非伺服器明講:
沒列進去會怎樣:res.headers.get('X-Session-Token-Refresh') 永遠回 null。 不報錯、不進 log,續期靜默失效——使用者每 12 小時被登出一次,而你查不出原因, 因為程式碼看起來完全正確。
同理,請求端的 X-Session-Token 要在 allowedHeaders 裡,否則預檢就被擋。 這種「兩邊都對、中間那層沒開」的 bug 最難查。 驗法很簡單,一行 curl 就看得到: ⑥ sessionVersion — 撤銷能力
payload 裡的 v 對應資料庫 StaffInfo.sessionversion,每次請求比對:
版本一升,那個人所有已發出的 token 立刻全部失效。這是 stateless token 唯一的撤銷手段 ——因為伺服器不存 session,你沒辦法「刪掉某個 session」,只能讓舊的簽章對不上。
但這次查證發現:整個 codebase 沒有任何地方會把 sessionVersion 加 1。
機制完整、比對也真的在跑,就是沒有觸發點。所以「改密碼後舊 session 失效」「離職立刻踢出」 這兩件事目前不會發生——不是壞了,是還沒接。
這是寫這篇時最值得記下的一筆:能力存在 ≠ 能力被使用。 sessionVersion 和 SESSIONTOKENENFORCE 是同一種病的兩個症狀—— 基礎建設做好了,最後那哩沒有人走完,而且沒有任何錯誤訊息提醒你。 四、開之前一定要先做的事
準備打開開關前,順手搜了前端怎麼處理 401:
意思是開了之後,被登出的人看到的不是「請重新登入」,而是:畫面照樣渲染、 資料全空、按鈕沒反應。使用者不知道自己被登出,只會覺得系統壞了。
所以順序必須是 先補前端 401 → 部署 → 再開 enforce。補的時候三個細節不能省:
另外要先算「開了會影響幾個人」。 沒有 log 查詢管道時可以換代理指標: token 機制是某天上線的,所以「最後登入時間早於那天」的人手上一定沒有 token。 一句 SQL 就問得出來,而且要數不重複人數,不是數請求佔比—— 登入態長期存在,無 token 請求可能只佔 2%,但那 2% 背後是 100% 的人各自被登出一次。 什麼時候不要這樣做 CORS exposedHeaders 沒設好,不要開。續期會靜默失效,變成每個人固定被登出, 而且查不出原因。開之前先用 curl 確認那個 header 真的露出來。 前端沒有 401 處理,不要開。使用者會看到壞掉的畫面而不是提示。 沒算過影響人數,不要開。這件事的成本會隨使用者數量成長—— 早期開只影響個位數,推廣之後開就是幾百人同時被登出。 不要在上班時間開。加環境變數會重啟服務,實測 60–80 秒 API 全掛。 不要以為 payload 看不到。base64url 是編碼不是加密,任何人都能解開讀內容。 安全性來自簽章驗不過,不是來自看不懂。 本篇由 claude-code(claude-opus-5) 於開發過程中產生。 repo:RedBallFlow @ 4ed37e77 標籤:auth session-token hmac security env-config