
這篇是後端視角:後端怎麼「信任 gateway 塞進來的身分」、怎麼在不查 DB 的前提下做授權、那條信任鏈的風險在哪。Kong 那段 Lua 怎麼寫、mesh 裡怎麼做,屬於 API Gateway 那邊,這篇不展開。素材是我自己的 micro-service proto:Kong 的 post-function、shared 套件的 auth 模組、auth-service 的 RS256 簽發。
Why:每個微服務各自驗 JWT,痛在哪
單體時代,驗證使用者身分就是一個 middleware 的事。拆成微服務之後,如果每個服務各自驗 JWT:
- 驗證邏輯(簽章演算法、過期判斷、claim 解析)在 user-service、post-service、auth-service…重複 N 份
- 簽章密鑰要散佈到每個服務——密鑰暴露面積 = 服務數量
- 改一次驗證規則(例如換演算法、加一個 claim 檢查)要動 N 個 repo
- 每個新服務都要「記得」把驗證接上,漏一個就是一道沒鎖的門
老實說,這個 proto 我一開始就把驗證放在 Kong,不是踩了痛才搬。當時的想法只有一句:「放 gateway 比較合理」。但要我講「不放那邊會撞到什麼坑」,我當時講不出來。上面那四條是寫這篇的時候回頭補的——所以這篇的「為什麼」是事後整理,不是事前想清楚的。我覺得這很常見:架構上的直覺常常是對的,但直覺不會告訴你它擋掉了哪些坑,要等你把坑列出來才知道自己躲過了什麼。
這是什麼:驗證上移到 gateway,後端只信 header
實際做法(對應 proto 裡 config/kong/kong.yml 的 post-function 和 shared 套件的 auth.py):
- Kong 在 gateway 層驗 JWT(用 public key 驗簽、驗過期)
- 驗過後,用一段
post-functionLua 把 JWT payload 的sub / email / roles / permissions塞進X-User-*request header - 後端服務完全不碰 token——它只讀
X-User-Id/X-User-Roles/X-User-Permissions這些 header,當作「gateway 已經幫我驗過的可信身分」
一句話:token 在 gateway 就地消化,後端看到的是已認證的身分,不是一顆還要自己拆的 token。
做過才知道的坑:不先 clear header,就是水平越權
這是整個設計最關鍵、最容易漏的一步(kong.yml 的 post-function 先 clear_header 五個 X-User-* 才重新塞):
如果 gateway 沒有先把 client 傳進來的
X-User-*header 清掉,那麼攻擊者只要自己在 request 裡塞一個X-User-Roles: super-admin,而後端「信任 header」——直接水平/垂直越權。
因為後端被設計成「無條件信任 X-User-*」,那條信任鏈的唯一守門點就是 gateway 必須保證這些 header 只能由它自己產生。漏掉 clear_header = 把偽造大門打開。
寫到這裡我才發現自己沒想過一件事:後端無條件信 X-User-*,那「誰能直接打到後端」就是整條信任鏈的邊界,不是只有 clear_header 一條線。
回頭查自己的 proto,兩個環境答案不一樣:
docker/docker-compose.yml把 user-service(8001)、post-service(8002)、auth-service(8003)都直接開到 host。本機開發很方便,但這代表繞過 Kong、直接打 8001、自己帶一個X-User-Roles: super-admin,後端就收了。k8s/base/那份是對的:三個服務的 Service 都是ClusterIP,只有 Kong 是NodePort。外面進不來,但叢集裡任何一個 pod 還是能直打——沒有 NetworkPolicy 把「只准 Kong 打後端」寫死。
所以防線其實是兩條:Kong 先清掉 client 帶的 header,加上後端只能從 Kong 進得來。第二條我 compose 版根本沒做,K8s 版做了一半。這篇寫完我才知道的。
為什麼是 RS256(非對稱)而不是 HS256
- auth-service 用 private key 簽發 access(30m)/refresh(7d) token
- Kong 只拿 public key 驗
- 關鍵論點:微服務下,gateway 只需要「驗證」不需要「簽發」——給它 public key 就好,私鑰只留在 auth-service。 這是「為什麼非對稱不是為了潮」的正解:HS256 的對稱密鑰一旦要讓 gateway 驗,就得把「能簽發 token 的密鑰」也給它,等於多一個能偽造身分的點。
我特地翻了 git 歷史確認:RS256 是一開始就選的,沒有「從 HS256 換過來」的故事。唯一一處 HS256 字樣是 env 範例檔留過一行 JWT_SECRET_KEY= # HS256 用(過渡期,之後移除),程式碼本身從頭到尾只有 RS256。理由就是上面那句:Kong 只要驗、不用簽,給它 public key 就夠。
key rotation 沒做。沒有 kid、沒有 JWKS endpoint,Kong 的 public key 是寫死在 configmap 裡的。換 key 等於所有現行 token 一起失效、Kong 也要重新部署。這是待辦,不是取捨——我只是還沒撞到需要換 key 的那天。
附帶的取捨:無狀態 RBAC,省 DB query 換權限延遲
- 權限在 login 當下算好、塞進 JWT,後端授權時只是 split 一個逗號字串來比對,完全不查 DB
- 好處:省掉每個請求 N 次權限查詢
- 代價:改一個人的權限,要等他的 access token 過期(30 分鐘)才生效
我不覺得這 30 分鐘一定要吞。實務上改權限的當下同步寫一筆到 Redis,後端每個 request 多查一次,就能即時生效。proto 裡的 token_blacklist.py 其實已經是這個形狀了:Redis SETEX、每個帶 Bearer 的 request 查一次,只是現在只有登出會寫進去。
要付的代價有兩個。一是每個 request 多一次 Redis 往返,比查 DB 便宜很多,但「無狀態」這個賣點就打折了——嚴格說我早就有一個 Redis 在路徑上,只是沒拿它做這件事。
二是故障方向。這個查詢目前是 fail-open:Redis 掛了就放行,登出的 token 在那段時間會活過來,即時撤銷的權限也一樣。只做登出的時候,這個代價還吞得下;要靠它做權限撤銷,這個決定就得改。
判斷的問題只有一個:這個檢查擋的是「量」還是「撤銷」?擋量的(rate limit、登入爆破計數),或者資料可以重算的(權限快取退回 DB,DB 才是事實來源),可以 fail-open,掛了只是放寬一點或慢一點。擋撤銷的不行,放行就等於撤銷沒發生過,應該 fail-closed:Redis 掛的那段時間,帶 token 的請求一律回 503。代價是 Redis 從加速器變成認證路徑上的命脈,它的可用性要跟著拉高。快取那一側為什麼可以 fail-open,見 CacheService 抽象層。
還有一個容易放錯的地方:方向要由呼叫端決定,不能交給共用的 cache 層。同一個 Redis client 可能同時被該 fail-open 的 rate limit 和該 fail-closed 的黑名單使用,共用層一律把例外吞掉,等於把黑名單拆了。
相關文章
- 回本章:B10 認證與授權
- 換 key 的那天真的來了會怎樣:token 驗不過的三個原因
- Gateway 那一側:API Gateway & Routing、Kong 踩雷集
- 同樣「放在 gateway 層還是應用層」的取捨:Rate Limiting
- Token 是怎麼來的:OAuth 2.0 授權機制
