JWT 安全最佳實踐:避開 10 個常見陷阱
在現代的微服務架構與單頁應用程式(SPA)中,JSON Web Token (JWT) 已成為身份驗證(Authentication)與授權(Authorization)的事實標準。JWT 的無狀態(Stateless)特性,讓伺服器無需在記憶體或資料庫中儲存 Session,極大地提升了系統的擴展性(Scaldenility)。
然而,JWT 的便利性往往伴隨著安全風險。許多開發者在導入 JWT 時,往往只關注如何「產生」與「解析」Token,卻忽略了「如何防禦攻擊」。一旦實作不當,JWT 可能會變成駭客入侵系統、竊取使用者個資的捷徑。
本文將深入探討 JWT 安全開發的 10 個常見陷阱,並提供專業的防禦建議,幫助你打造一個堅不可摧的身份驗證機制。
為什麼 JWT 的安全性至關重要?
JWT 的核心價值在於其「自包含」(Self-contained)的特性。Token 的 Payload 中包含了使用者的身分資訊(Claims),只要簽章(Signature)是正確的,伺服器就可以信任這些資訊。
但請記住一個基本原則:JWT 的 Payload 是 Base64 編碼,而不是加密。 任何人只要拿到你的 Token,都可以輕易地透過 JWT 解碼工具 讀取其中的內容。因此,JWT 的安全性完全建立在「簽章的完整性」與「金鑰的保密性」之上。如果簽章被偽造,整個身分驗證機制將會全面崩潰。
深入解析:JWT 安全開發的 10 個常見陷阱與對策
陷阱 1:在 Payload 中存放敏感資訊
這是新手最常犯的錯誤。開發者常為了方便,直接將使用者的密碼、身分證字號、甚至是信用卡號放入 Payload 中。
- 風險: 由於 JWT 的 Payload 是公開可讀的,任何攔截到 Token 的人(透過 XSS 或中間人攻擊)都能立即獲取這些敏感資訊。
- 最佳實踐: Payload 應該只包含「非敏感」的識別資訊,例如
user_id或role。如果需要存取敏感資料,請透過user_id從後端資料庫中查詢。
陷阱 2:使用過於簡單或弱小的簽章金鑰 (Secret Key)
JWT 的安全性高度依賴於簽章演算法所使用的 Secret Key。如果你的金鑰是 123456 或 my_secret_key,駭客可以利用暴力破解(Brute Force)或字典攻擊來推算你的金鑰。
- 風險: 一旦金鑰被破解,駭客就能自行簽製偽造的 Token,並在你的系統中偽裝成任何使用者(包括管理員)。 . 最佳實踐:* 使用高熵(High Entropy)的隨機字串作為金鑰。建議使用至少 256 位元的隨機字串,並將其儲存在環境變數(Environment Variables)或專門的金鑰管理服務(如 AWS KMS, Azure Key Vault)中,絕對不要將金鑰寫死在程式碼裡。
陷阱 3:未嚴格驗證 alg 標頭 (Algorithm Switching Attack)
這是一個經典的安全性漏洞。JWT 的 Header 中包含 alg 欄位,用來告訴伺服器應該使用哪種演算法來驗證簽章。
- 風險: 攻擊者可以將 Header 中的演算法從
RS256(非對稱加密)修改為HS256(對稱加密),並利用伺服器的公鑰來重新簽製 Token。如果你的伺服器端沒有強制檢查演算法,它可能會誤用錯誤的邏輯進行驗證,導致驗證通過。 - 最佳實踐: 在驗證 Token 時,程式碼必須明確指定預期的演算法。例如,如果你預期使用
HS256,請在驗證函式中加入參數限制,不接受任何其他演算法。
陷阱 4:缺乏適當的過期機制 (Expiration)
如果一個 JWT 的有效期(exp claim)被設定為永久有效,或者時間過長(例如一年),那麼一旦該 Token 遭到竊取,駭客將擁有長期的非法存取權。
- 風險: 無效的過期機制大幅增加了 Token 被盜後的攻擊窗口(Window of Opportunity)。
- 最佳開發實踐: 實施「短效 Access Token + 長效 Refresh Token」的策略。Access Token 的壽命應設定在數分鐘到數小時之間,而 Refresh Token 則用於換取新的 Access Token,並具備更嚴格的控管機制。
陷阱 5:將 JWT 存放在容易遭受 XSS 攻擊的地方
在前端開發中,許多人習慣將 JWT 存放在 localStorage 或 sessionStorage 中,因為這非常方便讀取。
- 風險:
localStorage容易受到跨站腳本攻擊(XSS)的威脅。如果你的網站存在 XSS 漏洞,攻擊者可以輕易透過 JavaScript 腳本讀取並竊取儲存在localStorage中的 Token。 - 最佳實踐: 建議將 JWT 存放在
HttpOnly且標記為Secure的 Cookie 中。HttpOnly屬性能防止 JavaScript 存取該 Cookie,從根本上降低了 XSS 竊取 Token 的風險。
陷阱 6:忽略了 iss (Issuer) 與 aud (Audience) 的驗證
在複雜的微服務架構中,多個服務可能會共用同一個身份驗證機制。
- 風險: 如果沒有驗證
iss(發行者)和驗證aud(接收者),攻擊者可能會利用從「服務 A」取得的合法 Token,去嘗試存取「服務 B」。 - 最佳實踐: 嚴格檢查
iss是否為你信任的授權伺服器,並檢查aud是否符合當前服務的識別碼,確保 Token 是專門發送給該服務使用的。
陷阱 7:允許使用 none 演算法
在 JWT 的規範中,存在一種 alg: "none" 的演算法,這原本是用於某些特殊測試場景。
- 風險: 如果後端驗證邏輯不夠嚴謹,攻擊者可以構造一個 Header 為
{"alg": "none"}的 Token,並移除簽章部分。如果伺服器接受了這種 Token,攻擊者就能隨意修改 Payload 中的身分資訊。 - 最佳實踐: 在開發環境中絕對禁止使用
none演算法,並在生產環境的驗證邏輯中明確拒絕任何alg: "none"的請求。
陷阱 8:缺乏 Token 撤銷 (Revocation) 機制
JWT 的無狀態特性是一把雙面刃。一旦 Token 發出,除非它過期,否則伺服器很難主動讓它失效。
- 風險: 當使用者修改密碼、登出或發現帳號被盜時,如果無法立即撤銷現有的 Token,攻擊者仍能繼續使用舊的 Token 進行操作。
- 驗證的最佳實踐:雖然 JWT 追求無狀態,但為了安全,你仍需要建立一個「黑名單(Blacklist)」機制。可以使用 Redis 儲存已登出或被封鎖的 Token ID (
jti),並在驗證 JWT 時先檢查該 ID 是否在黑名單中。
陷 9:未落實 Refresh Token 的安全輪轉 (Rotation)
Refresh Token 的權限較大,且壽命較長,因此它是攻擊者的首要目標。
- 風險: 如果 Refresh Token 被盜且沒有輪轉機制,攻擊者可以無限期地產生新的 Access Token。
- 最佳實踐: 實施 Refresh Token Rotation。每當使用者使用 Refresh Token 換取新的 Access Token 時,伺服器同時也發放一個「新的 Refresh Token」,並使舊的 Refresh Token 失效。如果偵測到舊的 Refresh Token 被重複使用,則應立即撤銷該使用者所有的 Token。
陷阱 10:在非 HTTPS 環境下傳輸 Token
這看似基本,但仍有開發者在測試環境或內部網路忽略此點。
- 風險: 在 HTTP 環境下,Token 會以明文形式在網路中傳輸,極易遭受中間人攻擊(MITM),導致 Token 被攔截。
- 最佳實踐: 全站強制使用 HTTPS。確保所有 API 請求都經過加密傳輸,保護 Token 在傳輸過程中的完整性與機密性。
進階實戰:如何設計一套安全的 JWT 架構?
要建立一個企業級的 JWT 驗證系統,你不能只依賴單一的技術,而需要一套組合拳。
選擇正確的簽章演算法:HS256 vs RS256
在選擇演算法時,你需要權衡安全性與複雜度。
| 特性 | HS256 (Symmetric) | RS256 (Asymmetric) |
|---|---|---|
| 演算法類型 | 對稱加密 (Symmetric) | 非對稱加密 (Asymmetric) |
| 金鑰使用 | 簽章與驗證使用同一個 Secret Key | 簽章使用 Private Key,驗證使用 Public Key |
| 安全性 | 較低(金鑰一旦洩漏,攻擊者可偽造) | 較高(即使 Public Key 洩漏,也無法偽造) |
| 效能 | 非常快 | 較慢(計算量較大) |
| 適用場景 | 單一伺服器的簡單應用 | 微服務、第三方整合、需要分發驗證權限的場景 |
建議: 如果你的系統包含多個獨立的微服務,強烈建議使用 RS256。這樣,身份驗證伺服器只需持有 Private Key,而各個微服務只需持有 Public Key 即可進行驗證,極大地降低了金鑰外洩導致的風險。
實作範例:Node.js 安全驗證邏輯
以下是一個使用 jsonwebtoken 套件進行安全驗證的範例,展示了如何防止演算法切換攻擊:
const jwt = require('jsonwebtoken');
// 這是從環境變數讀取的強大金鑰
const SECRET_KEY = process.env.JWT_SECRET_KEY;
/**
* 安全的 Token 驗證函式
* @param {string} token - 使用者傳入的 JWT
* @returns {object|null} - 回傳解碼後的 Payload,失敗則回傳 null
*/
function verifyTokenSecurely(token) {
try {
// 關鍵點:明確指定演算法為 HS256,防止 alg: none 或 RS256 切換攻擊
const decoded = jwt.verify(token, SECRET_KEY, {
algorithms: ['HS256'],
issuer: 'https://auth.supertools.tw', // 驗證發行者
audience: 'https://api.supertools.tw' // 驗證接收者
});
return decoded;
} catch (error) {
console.error('JWT 驗證失敗:', error.message);
// 這裡可以針對不同錯誤類型(如 TokenExpiredError)進行處理
return null;
}
}
// 測試範例
const myToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // 假設的 Token
const user = verifyTokenSecurely(myToken);
if (user) {
console.log('驗證成功,使用者 ID:', user.sub);
} else {
console.log('無效的 Token');
}
在開發過程中,如果你需要快速檢查 Token 的結構或內容是否符合預期,可以使用我們的 JWT 測試工具 來進行即時的除錯與驗證。
開發者必備的 JWT 檢查工具
在開發與除錯階段,能夠直觀地看到 Token 的內容與結構非常重要。
- JWT 解碼工具 (JWT Decoder):
當你在處理前端邏輯,需要確認 Payload 中的
claims是否正確包含role或exp時,這個工具能讓你快速將 Base64 字串還原成可讀的 JSON 格式。 - JWT 測試與除錯器: 用於模擬不同的簽章演算法與金鑰,幫助你在開發初期就發現演算法不匹配或簽章錯誤的問題。
FAQ:關於 JWT 安全性的常見問題
Q1: 我可以把 JWT 存放在 Cookie 嗎?
A: 可以,而且非常建議。請務必設定 HttpOnly 與 Secure 屬性,以防止 XSS 攻擊與中間人攻擊。
Q2: 如果我的 Access Token 被盜取了,該怎麼辦? A: 這是 JWT 的天生弱點。你必須透過「黑名單機制」將該 Token 加入失效清單,並且應盡快通知使用者更改密碼,並透過 Refresh Token 輪轉機制強制登出所有裝置。
Q3: 為什麼我不應該在 JWT 中放 User Role? A: 其實可以放,但僅限於「非敏感」的權限資訊。如果權限變更非常頻繁,建議在後端進行二次檢查,因為 JWT 的內容在過期前是無法即時更新的。
Q4: 使用 RS256 會比 HS256 慢很多嗎? A: 在現代伺服器效能下,這種差異對於大多數 Web 應用來說幾乎可以忽略不計。安全性帶來的收益遠大於那點效能損失。
Q5: 如何生成足夠強大的 Secret Key?
A: 你可以使用 Node.js 的 crypto 模組來生成:require('crypto').randomBytes(64).toString('hex')。
Q6: JWT 驗證失敗時,應該回傳什麼 HTTP 狀態碼?
A: 建議回傳 401 Unauthorized(表示身分驗證失敗)或 403 Forbidden(表示身分已確認,但權限不足)。
結論
JWT 是一項強大的技術,能為分散式系統帶來極大的便利,但它並非「設定完畢後即可高枕無憂」的技術。安全性不應僅僅依賴於演算法本身,更取決於開發者在實作過程中的細節處理。
請記住:不要存放敏感資料、嚴格限制演算法、使用強大的金鑰、實施 Token 輪轉,並善用 HttpOnly Cookie。 只要遵循這些最佳實踐,你就能在享受 JWT 帶來的擴展性優勢時,同時確保系統的安全性。