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 的內容與結構非常重要。

  1. JWT 解碼工具 (JWT Decoder): 當你在處理前端邏輯,需要確認 Payload 中的 claims 是否正確包含 role 或 exp 時,這個工具能讓你快速將 Base64 字串還原成可讀的 JSON 格式。
  2. 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 帶來的擴展性優勢時,同時確保系統的安全性。