簽署與驗證 JWT
JWT 是三段 base64url——標頭、載荷、簽名——前兩段持有權杖的任何人都能讀。解碼告訴你它聲稱了什麼;只有第三段告訴你該不該相信它。本文談的就是第三段:簽名如何產生、誰能產生它、驗證方如何檢查它,以及「已驗證」在何處不再等於「值得信任」。你可以在JWT 簽署與驗證器中親自執行每一步,它在你的瀏覽器裡本機簽署與驗證。
解碼不是驗證
關於 JWT 最常見的誤解,也是最危險的誤解,就是以為讀取權杖等於檢查權杖。其實不是。標頭與載荷是 base64url——一種編碼,不是加密——所以攔截到權杖、在記錄檔中找到它,或從瀏覽器的 local storage 複製它的人,不需要任何金鑰,幾秒鐘就能解碼。裡面的宣告(給誰、何時到期、聲稱什麼角色)都是斷言,而斷言很廉價。
簽名存在的理由,正是因為宣告很廉價。它是標頭、載荷與只有簽發方持有的金鑰之間的密碼學綁定:改動載荷的一個字元,簽名就不再相符。驗證就是重算這道數學——用簽發方的金鑰,對收到的標頭與載荷重新計算簽名,再比對。如果相符,權杖就與簽發方簽署時一模一樣;如果不相符,一定有東西被改過,權杖再怎麼看似合理也一文不值。
對稱與非對稱:誰能鑄造權杖
用 HS256 時,簽署用的密鑰就是驗證用的密鑰。這讓 HMAC 快速、簡單,在同一個服務內部完全夠用——你的 API 簽署自己的權杖、自己檢查,密鑰永不離開大樓。第二方需要驗證的那一刻,模式就破了:把密鑰交給他們,就等於把鑄造權杖的能力交給他們,因為驗證與簽署是同一種運算。洩漏的 HMAC 密鑰不是驗證問題,是偽造問題。
非對稱演算法把兩種能力拆開。RS256、ES256 與 EdDSA 用私鑰簽署、用公鑰驗證,所以簽發方可以公開發布公鑰——以 JWKS 的形式——全世界的驗證方都能檢查權杖,卻永遠拿不到偽造權杖的能力。這個拆分正是 JWT 能跨服務運作的原因:你的 API 信任來自身分提供者的權杖,是因為它信任該提供者發布的金鑰,而不是因為它與對方共享密鑰。代價是複雜度——金鑰格式、輪換、kid 比對——而本站的簽署與驗證器正是為了讓這些具體可見而存在。
演算法混淆攻擊
經典的 JWT 攻擊不需要破壞任何密碼學——它只需要一個信任權杖自己指明演算法的驗證方。攻擊者取一份合法的 RS256 權杖——它的公鑰本來就是公開的——再用 HS256 重新簽署相同的載荷,把公鑰本身當作 HMAC 密鑰。一個從標頭讀取 alg 再據此選擇驗證程序的驗證方,現在會用 HMAC「驗證」這份偽造權杖,把公鑰當作密鑰——而公鑰是公開的。
防禦是結構性的,不是建議性的:驗證方必須釘選它接受的演算法,而且金鑰家族必須與演算法家族相符——RSA 金鑰永遠不能驗證 HMAC 權杖,密鑰也永遠不能驗證 RSA 權杖。一個會把金鑰跨家族重新解讀的驗證方,在設計上就是壞的,無論它的密碼學多正確。這正是本站驗證器遇到跨家族金鑰時會明確警告並拒絕檢查的原因:這項拒絕本身就是功能。
JWKS 與 kid 輪換在實務中的樣子
身分提供者不會只發一把永恆的金鑰;它發布一組——JWKS,通常位於 /.well-known/jwks.json——加入新金鑰來輪換,經過一段寬限期後再移除舊金鑰。每份權杖的標頭都帶有 kid,指明簽署它的金鑰,所以驗證方擷取金鑰集、挑出相符的金鑰、再驗證。用舊金鑰簽的權杖在舊金鑰被移除前都繼續有效,這正是輪換安全、而不是一次翻天覆地的原因。
出問題的往往是操作細節。驗證方必須快取 JWKS(每次請求都擷取一次,等於自己製造斷線)、處理 kid 未知的情況——通常是快取還沒看到的一次輪換,所以失敗前先重新擷取——而且絕不接受來自不受信任 URL 的金鑰集:金鑰集的可信度只取決於它經過的通道,這正是 JWKS 端點放在 HTTPS 後面的原因,也是本站JWKS 建立器把每個 kid 導出為金鑰本身的 RFC 7638 指紋的原因:id 無法脫離金鑰被獨立偽造。
exp、nbf、iat——以及時鐘問題
簽名證明權杖曾被簽署;它沒說權杖是否還應該被接受。那是時間宣告的工作:exp(何時失效)、nbf(何時開始生效)、iat(何時簽發)。只檢查簽名、跳過時鐘的驗證方,會永遠接受一份權杖——包括一年前被偷的那一份。實務上的答案是短效期:存取權杖以分鐘計,長效信任由重新整理權杖承擔,撤銷就靠不再重新簽發。
時鐘彼此不一致,所以驗證方會留一點寬容——通常對 exp 與 nbf 寬容一兩分鐘——但寬容是對漂移的容忍,不是忽略到期的許可。裝置時鐘差了幾分鐘,就會產生自己的伺服器拒絕的權杖,而 iat 在未來的權杖是偽造訊號,不是四捨五入的誤差。同樣的時鐘紀律也適用於工具:驗證器對照你看得到的時鐘檢查到期時間,權杖到期時會直說到期,而不是含糊地說「無效」。
「已驗證」能證明什麼、不能證明什麼
已驗證的權杖只證明三件事,別無其他:標頭與載荷與簽署時一模一樣、簽署者持有金鑰、權杖在有效期限內。它不證明簽發方就是你以為的那個——那取決於金鑰從哪裡來,而從攻擊者控制的 URL 擷取的金鑰,驗證攻擊者的權杖一樣完美。它不證明宣告為真——簽名認證的是陳述,不是陳述背後的事實。它也不證明權杖沒有被偷:任何人出示有效的權杖都會被接受,這正是權杖短效且有範圍的原因。
所以信任的決定在權杖之外。你的 API 信任一份權杖,是因為它透過 HTTPS 從身分提供者的 well-known URL 擷取金鑰、因為 iss 與它設定的提供者相符、因為 aud 與它自身的服務相符。驗證是最後一道檢查,不是第一道:金鑰來源、簽發方、對象,然後才是簽名、然後是時間。一個只報「有效」而不給脈絡的工具,說的是數學的實話,關於信任卻什麼也沒說——這正是本站驗證器在判決旁邊寫明這一點的原因。
常見問題
沒有金鑰也能驗證權杖嗎?
不行——驗證需要簽發方的公鑰(HMAC 則需要共享密鑰)。解碼不需要金鑰,這正是要避免的混淆:解碼過的權杖只是可讀,不是已驗證。如果簽發方發布 JWKS,請從 well-known URL 擷取它,並比對權杖的 kid;本站的驗證器會替你做這件事。
為什麼我的 RS256 權杖用密鑰驗證會失敗?
因為密鑰和 RSA 金鑰屬於不同家族,健全的驗證方拒絕混用。RS256 權杖要用簽發方的 RSA 公鑰驗證——PEM、JWK 或 JWKS——絕不是用文字密鑰。用密鑰驗證正是演算法混淆攻擊:公鑰是公開的,如果 HMAC 驗證接受它當密鑰,任何人都能偽造權杖。
JWT 應該活多久?
存取權杖以分鐘計,不要以天計:5–15 分鐘是常見範圍,較長的會話由重新整理權杖承擔。短效期把被偷權杖的損害限制在到期前的窗口內,撤銷也變成「不再重新簽發」的事,而不必維護封鎖清單。活上好幾週的權杖,是工具更差的會話 cookie。