JWT 時間驗證:用 exp、iat、nbf 判斷有效期
DDEVELOPER
開發發布: 7 分鐘閱讀

JWT 時間驗證:用 exp、iat、nbf 判斷有效期

一個剛簽發、有效期一小時的 JWT,為甚麼會即時被 API 拒絕?先看驗證端報的是「尚未生效」還是「已過期」。如果驗證端的時鐘慢了幾秒,延長到期時間也無法讓它提早通過生效時間的檢查。以下用同一組時間值和一條預先發出的邀請連結,說明五個容易混淆的地方。

日文原文發布: 2026-04-17

先看清楚:時間值以秒計

JWT(JSON Web Token)讓簽發者將一組聲明(claim)交給接收方。每項聲明由名稱和值組成:邀請權杖用 sub 指明受邀用戶,用 exp 記下到期時間。讀到這些資料,只代表你知道權杖聲稱甚麼;服務仍須核實。

本文採用 JWS 的緊湊格式。它以句點分隔三部分:標頭、載有聲明的 JSON 內容,以及簽署值。三部分均以 Base64url 編碼。任何持有人都可以不用密鑰解碼首兩部分,讀取當中資料。JWT 也可以採用 JWE 格式加密內容。

以範例的 HS256 為例,簽發端以共用密鑰,對「已編碼的標頭、句點、已編碼的內容」計算 HMAC-SHA-256。接收端的程式庫以同一密鑰計算,並與第三部分核對。更改 exp 會令核對失敗;時間流逝卻不會改變簽署值,所以 API 還須按用途、接收對象及驗證端的時鐘作判斷。持有共用密鑰的一方也能簽發新權杖,切勿將密鑰交給受邀用戶的瀏覽器。

RFC 7519 的 NumericDate 是由協調世界時(UTC)1970 年 1 月 1 日零時起計的秒數,略過閏秒,可包含小數。JavaScript 的 Date.now() 回傳毫秒;要取得整數秒作為簽發時間,可用 Math.floor(Date.now() / 1000)。在 RFC 7519 中,exp、iat 和 nbf 都是選用欄位;應用程式須按用途決定哪些欄位必須存在。

如表格未能完整顯示,可左右捲動。

欄位含義怎樣理解
exp到期時間不設寬限時,由這一刻起拒絕。
iat簽發時間簽發者建立這個權杖的時間。
nbf生效時間不設寬限時,最早可在這一刻使用。
auth_time身份驗證時間(OpenID Connect)用戶完成身份驗證的時間。

以下是用來比較時間的 JWT 內容(payload)。後面的邀請連結範例還會驗證簽署,並核對權杖是發給哪個服務及哪位用戶。

{
  "sub": "user_123",
  "iat": 1713340800,
  "nbf": 1713340800,
  "exp": 1713344400
}

一、時鐘慢了,剛簽發的權杖可能仍未生效

上例中 iat 等於 nbf,exp 比它們遲一小時。假設其餘驗證均通過,又不設寬限:剛簽發時,驗證端的時鐘快五秒,仍在有效期內;慢五秒,則因為未到 nbf 而拒絕。若錯誤訊息確實是「已過期」,請先把驗證端的目前時間和 exp 換算成秒,再比較兩者。

RFC 7519 第 4.1.4 及 4.1.5 節容許實作因應時鐘偏差設定少量寬限。下表列出 jsonwebtoken 9.0.2 及 jose 6.1.0 對上例的實測結果,30 秒是範例設定。

如表格未能完整顯示,可左右捲動。

驗證端的時間寬限 0 秒寬限 30 秒
距離 nbf 尚有 30 秒尚未生效通過
剛好等於 nbf通過通過
剛好等於 exp已過期通過
exp 過後整整 30 秒已過期已過期

可在 jsonwebtoken 的 verify 或 jose 的 jwtVerify 設定 clockTolerance: 30。增加寬限會同時放寬兩端:權杖可以提早使用,也可以在到期後繼續使用一段時間。應監察伺服器的時鐘偏差,再決定服務可接受的範圍。

二、剛簽發權杖,不等於用戶剛登入

今天換取的新權杖,可以屬於昨天已登入的用戶。在 OpenID Connect 的認證請求中指定 max_age 時,傳回的 ID 權杖必須包含 auth_time。要計算用戶上次完成身份驗證距今多久,應以該時間為準。

假設驗證時間為 now,我們將簽發時間設為 iat = now + 120,即驗證當刻之後兩分鐘。在其他條件成立、沒有設定權杖最長使用時間的情況下,上述兩個版本均接受這個權杖。再把上限設為 300 秒、寬限設為零,jsonwebtoken 的 maxAge 仍然接受,jose 的 maxTokenAge 則拒絕。若服務不接受未來的簽發時間,應用實際採用的程式庫測試這項要求。單一權杖的有效期和整個登入階段的期限也應分開處理。

三、預先寄出的邀請,何時開始計七日?

假設現在發出邀請,八日後才開放,再由開放一刻起計七日。jsonwebtoken 的 expiresIn: "7d" 一般由簽發時間起計。這樣設定,權杖會在開放前一日到期。要讓受邀用戶有完整七日,先由開放時間算出 nbf,再由 nbf 算出 exp。

// jwt 指 jsonwebtoken;releaseDate 是指定開放時間的 Date 物件。
// secret 是妥善保管的 HS256 共用密鑰,須使用至少 256 位元的隨機值。
if (typeof userId !== "string" || !userId) throw new Error("請指定受邀用戶");
const nbf = Math.ceil(releaseDate.getTime() / 1000);
if (!Number.isFinite(nbf)) throw new Error("開放時間無效");
const exp = nbf + 7 * 24 * 60 * 60;
const token = jwt.sign({ sub: userId, nbf, exp }, secret, {
  algorithm: "HS256", issuer: "invite-service", audience: "invite-api"
});

如果開放時間包含不足一秒的部分,程式會向上取整至下一個整秒,由該秒起計七日。向下取整則可能提早最多 999 毫秒開放。不設寬限時,驗證端由 nbf 起接受,到了 exp 即拒絕。若服務必須準時開放,還要同步驗證端的時鐘;即使寬限為零,時鐘過快仍會提早放行。

四、更新權杖與登入階段各有期限

JWT 的 exp 是固定時間。使用權杖或延長登入階段,都不會改動已簽發權杖內的 exp。OAuth 的更新權杖(refresh token)也可以只是一串不帶可讀欄位的字元,由伺服器保存上次使用時間及撤銷狀態。「閒置多久便失效」和「登入後最長可以用多久」是兩個不同條件。

授權伺服器可透過輪替偵測重用:每次換領時發出新的更新權杖,令舊權杖失效,並保留兩者的關係。若失效的舊權杖再次被提交,便撤銷同一系列仍有效的更新權杖。根據 RFC 9700,向公開客戶端簽發更新權杖時,授權伺服器須使用綁定發送方的權杖或採用輪替,以偵測重用。

五、讀到 exp,還要驗證簽署和受邀人

解碼器即使遇到無效簽署,也可以顯示權杖內容。只改 exp,畫面會顯示新的到期時間,但原有簽署無法通過驗證。接收端應使用受信任簽發者的密鑰,並指定服務接受的演算法、簽發者及接收對象。

// sessionUserId 取自已完成身份驗證的登入階段。
if (typeof sessionUserId !== "string" || !sessionUserId) {
  throw new Error("請先登入");
}
const claims = jwt.verify(token, secret, {
  algorithms: ["HS256"], issuer: "invite-service", audience: "invite-api",
  subject: sessionUserId, clockTolerance: 0
});
if (!Number.isFinite(claims.exp) || !Number.isFinite(claims.nbf)) {
  throw new Error("邀請權杖必須包含 exp 和 nbf");
}
if (claims.exp - claims.nbf > 7 * 24 * 60 * 60) {
  throw new Error("邀請有效期不可超過七日");
}

簽署正確的權杖也可能因簽發端設定出錯而有效期過長,所以 API 會檢查從生效至到期是否超過七日。在本例中,aud 是專門處理邀請的服務 invite-api;sub 是受邀用戶,必須與目前登入的用戶相符。如果同一簽發者和同一接收方處理不同用途的權杖,應按用途訂立不同的驗證規則。

有 exp 時檢查是否過期,與要求 exp 必須存在,是兩項檢查。在 jose 的 jwtVerify 中,可用 requiredClaims: ["exp", "nbf"] 要求兩者均存在。切勿由尚未驗證的標頭決定容許的演算法或密鑰來源。

如何測試自己的驗證流程

  • 先決定哪些欄位必須存在,以及是否限制權杖由簽發至使用的最長時間。
  • 在測試中固定驗證端時間,分別測試生效前一刻、生效當刻、到期前一刻及到期當刻。
  • 先竄改已簽署的內容,保留原有簽署;另外用受信任的測試密鑰,簽發接收對象錯誤或缺少必要欄位的權杖。檢查每次被拒絕的原因。
  • 輪換公鑰時,可用 kid 從已信任的簽發者密鑰集合中選取公鑰。每種用途應有對應的驗證條件。

通過驗證後仍須檢查操作權限

服務確認身份後,仍須判斷用戶有沒有權限執行這項操作。伺服器上的檔案另受 Unix 權限控制。例如,其他用戶能夠存取私鑰時,OpenSSH 可能拒絕使用該私鑰;常見做法是將權限設為 600。可參閱 chmod 檔案權限指南,比較 600、644 和 755 的適用情況。

參考資料及來源

編輯說明

本文使用 AI 協助編輯,並在發布前由編輯核對。內容仍可能包含事實、理解或時效上的錯誤;作出重要決定前,請查閱所列的一手資料或官方文件。

相關工具

相關文章