雜記 › 技術筆記 › JWT 那三段字串裡到底寫了什麼?header、payload 與 exp 的真實讀法

JWT 那三段字串裡到底寫了什麼?header、payload 與 exp 的真實讀法

上個月替同事看一個內部系統的登入介面。前端拿到 401,抓包裡 Authorization 標頭是 Bearer 加一長串、用 . 分成三節的字元,肉眼掃不出可讀的詞。他要的只有兩件事:這串東西什麼時候到期,role 裡是不是 admin。我先把這兩問拆開:「讀得到什麼」和「伺服器認不認」是兩條線,混在一起就排查不下去。

它是「編碼 + 簽章」,不是加密

前兩節是 base64url 編碼,不要金鑰誰都能還原成 JSON;第三節才是簽章,只有拿到對稱金鑰(HS 那一族)或對應公鑰(RS 那一族)才驗得了。這兩層常被壓成一句「加密了所以看不見」,其實相反:你看不見只因為還沒解碼,拿到串的人能看到全部欄位。樣例留下的三節長度並排抄出來:

Header    : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload   : 264 個 base64url 字元
Signature : 43 個 base64url 字元

三段對照:字元數、位元組數、能不能讀

段base64url 字元數解出後能不能讀出內容本批樣例裡的值
Header3627 位元組 / 27 字元能,不需要金鑰{"alg":"HS256","typ":"JWT"}
Payload264198 位元組 / 194 字元能,但讀出文字不等於可信iss、sub、aud、name、role、vip、iat、nbf、exp、jti
Signature4332 位元組 / 30 字元讀不出意圖樣例金鑰 your-256-bit-secret

載荷那行 198 位元組與 194 字元不是同一個數:name 的值是中文「張三」,UTF-8 下中文佔的位元組比字元多,role 則解出 ["admin","editor"]。手寫 base64url 解碼字元表一加就錯位,這步我交給 JWT 解析工具:三段各報位元組數與字元數,逐條解釋註冊聲明,解析和驗簽都在瀏覽器裡完成,頁面不發網路請求。

iat、nbf、exp 都是 UTC 秒

  • 有效樣例的 iat 與 nbf 都是 1792402758,換成 2026-10-19 09:39 UTC;兩值相等,簽發即生效。
  • 同一段的 exp 是 1861919999,等於 2028-12-31 23:59 UTC,相對腳本執行那次還剩 802.8944907407407 天。
  • 已過期樣例 iat 1748593800 對應 2025-05-30 08:30 UTC,exp 1748766600 對應 2025-06-01 08:30 UTC。
  • 那段的 exp 相對腳本執行時刻已過去 506.7513310185185 天,簽章卻依然是好的。

換算時別把秒當毫秒餵給日期函式;同一秒會給出 UTC 和本地兩行,先確認讀的是哪一行。想還原某一節內容可以用 Base64 編解碼,把秒數換成可讀日期用 時間戳轉換。

驗簽通過和沒過期是兩件事

已過期的那段樣例,簽章段仍是 32 位元組,用樣例金鑰 your-256-bit-secret 重算 HMAC,驗簽照樣寫「通過」。簽章鎖的是「簽發到現在,前兩節沒被改過一個字元」,不含時間語義;伺服器只驗簽不查 exp,很久前簽的權杖照樣能用。iss、aud 同理要伺服器自己比對,工具不會替你查,權限判斷更不在它職責裡。

三種邊界,長度先出賣了它們

樣例algkid簽章段位元組數那次執行列印的驗簽結果
有效HS256無32通過(secret = your-256-bit-secret)
已過期HS256無32通過,但 exp 落在 2025-06-01 08:30 UTC
alg=nonenone無0沒有簽章可驗
載荷不是 JSONHS256無32不匹配
RS256RS256demo-key-1256通過(用頁面內建公鑰)

alg=none 那行最值得盯:整串仍切成三節,最後一節解出 0 位元組,等於沒簽章。風險在老伺服器端套件按權杖自報的演算法驗簽——載荷改掉、演算法寫 none、簽章抹空,校驗就被繞過。載荷不是 JSON 的那種能解出原文「這是一段不是 JSON 的載荷文字」,但結構不成,驗簽報不匹配,腳本在那行後面直接打出重算的簽章 4prUH45MQAmbRVmdqHYpEsc2OzZ5OM0TEgApoZOXywI,跟權杖自帶那節一比就看出誰動過。

對稱金鑰的強度是能算出來的

HS 族同一把金鑰簽發和驗證,金鑰長度決定它扛不扛得住離線爆破。樣例那把 your-256-bit-secret 只是示範佔位:26 個小寫字母取 8 位,候選空間 208827064576 種,每秒試 100 萬次,58.01 小時掃完;字元表擴到大小寫加數字 62 個、取 12 位,空間 3.2262667623979e+21 種,每秒 10 億次也要 102421.2 年。前者是一個週末的量級,後者才勉強能指望。

幾個當時沒搞乾淨的問題

Q:這串字元是不是加密過的?誰能看到裡面的內容?

沒加密。前兩節只是 base64url 編碼,拿到的人都能還原成 JSON,它防篡改不防偷看。手機號碼、密碼、證件號應該只在載荷裡留 ID、伺服器端再查;鍵名撞上這些詞時頁面會提示,但攔住已簽出的權杖不是它做的事。

Q:base64 和 base64url 差在哪?

字元表不同:base64url 把 + 和 / 換成 - 和 _,結尾補位符 = 也不寫,一整節才能塞進 URL 和標頭。常見範例載荷裡那段 JSON 是 55 個字元,編成 base64url 後是原來的 1.345 倍。

Q:過期但驗簽通過的權杖,伺服器端到底認不認?

取決於伺服器端查不查。已過期那條就是典型組合:重算一致、驗簽通過,exp 卻是 2025-06-01 08:30 UTC。有效期、簽發者、受眾、是否撤銷各是各的檢查項,漏一項就可能放行。

Q:HS256 和 RS256 該選哪個?

自己簽自己驗,HS 族省事,代價是每個驗證方都握著同一把金鑰;公鑰要發給多個驗證方時用 RS 或 ES 族,避免共享金鑰外洩,代價是前面那 256 位元組的簽章。

回到同事那兩個問題。到期時間是從 exp 那行 UTC 讀出來的,不是從報錯碼猜的;role 是不是 admin,讀出來只說明簽發方當時這麼寫,介面認不認還得伺服器端說了算。我現在的取捨是載荷只放 ID 和一組角色名——多一個欄位多一處洩露面,簽章能防的只有篡改,不是偷看。

📎 相關文章

PNG 壓了三遍只減 426 位元組:無損壓縮的真實邊界 2026-10-22 WebP 轉 PNG 後體積漲了幾倍:什麼時候必須轉,怎麼把漲幅壓住 2026-10-22 截圖縮到四分之一,小字糊成一團的三種原因 2026-10-22 同樣裁 300×300,頭像為什麼總偏右下角半格 2026-10-22 按每月 1.7 倍預估 18 個月,腳本和表格為什麼給出兩個數 2026-10-22