JsonMind
安全与认证8 分钟阅读更新于 2026年11月9日

JWT 过期怎么排查:exp、iat、nbf 时间戳完全指南

理解 JWT 的 exp、iat、nbf 时间声明、秒与毫秒的区别和时钟偏移,快速定位 jwt expired、签名错误和算法不匹配。

JM作者:JsonMind Engineering最后复核: 2026年11月9日依据权威规范与引用来源复核。
快速答案

JWT 是否过期由 exp 声明决定,它是一个以秒为单位的 Unix 时间戳。先解码 payload,把 exp 转成可读时间,再和服务端当前时间比较;同时检查 nbf、iat、签名算法和密钥。解码不等于验签,未验签的声明不能用于鉴权。

核心要点
  • exp、nbf、iat 都是秒级 Unix 时间戳,不是毫秒。
  • Token 被拒绝可能是因为过期、尚未生效、签名错误或算法不匹配。
  • 解码只能看到声明,不能证明 Token 真实可信。
  • 分布式系统应允许 30 到 120 秒的时钟偏移。
  • 不要把完整 Token 写进日志,只记录指纹或必要声明。

JWT 报错看起来只有一个,但根因通常落在四个地方:Token 真的过期、Token 还没生效、签名不匹配、算法不符合预期。修错层不仅浪费时间,还可能削弱安全性。

这篇文章先讲时间声明,再讲如何安全地查看 Token,最后按常见错误逐项排查。

三个时间声明

JWT 的 payload 是一个 JSON 对象。注册的时间声明有:

声明含义单位
iat签发时间Unix 秒
nbf生效时间Unix 秒
exp过期时间Unix 秒

Token 被接受的条件是:

当前时间 >= nbf(如果存在)
当前时间 <  exp(如果存在)

这里最关键的是单位:秒,不是毫秒。exp: 1761500000000 大约对应公元 57770 年,exp: 1761500000 才是正常的人类时间。把秒和毫秒搞混,是“刚生成就过期”和“永远不过期”这两类问题的头号原因。

RFC 7519 把 NumericDate 定义为从 1970-01-01T00:00:00Z 起经过的秒数,忽略闰秒,并允许小数。多数库按数字直接比较,所以 Date.now() / 1000 不取整也能用;但日志里最好取整,不然时间差读起来很别扭。三个声明都是可选的:没有 exp 的 Token 永不过期,主流库也照样验签通过。access token 的校验端应该强制要求 exp,而不是指望每个签发方都记得加。

先解码,再验签

JWT 的 header 和 payload 只是 Base64URL 编码,并没有加密。没有密钥也能读:

function decodePart(part) {
  const padded = part.replace(/-/g, "+").replace(/_/g, "/")
    .padEnd(Math.ceil(part.length / 4) * 4, "=");
  return JSON.parse(atob(padded));
}

const [headerPart, payloadPart] = token.split(".");
console.log(decodePart(headerPart));
console.log(decodePart(payloadPart));

快速查看可以直接用 JWT 解析工具。把 exp 和当前时间对比,可以用 Unix 时间戳转换工具。解析工具应该明确提示“未验签”。

不要用“解码成功”作为鉴权依据。在验签之前,payload 完全由攻击者控制。

解码本身还有两个坑。JWT 片段是无填充的 Base64URL,长度不是 4 的倍数时直接调用 atob 会抛 Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded;上面的辅助函数先补了 =,所以能正常工作。另一个更隐蔽:atob 返回二进制字符串,每个字符代表一个字节,直接 JSON.parse 时,中文昵称之类的非 ASCII 字段会变成乱码。平时只调 exp、alg 这些 ASCII 声明所以看不出来,真正读取用户姓名时才会踩到。需要原文时,先把二进制字符串转成字节,再用 TextDecoder 按 UTF-8 还原。

错误一:jwt expired

Token 的 exp 已经早于校验方的当前时间。

排查步骤:

  1. 解码 payload,读取 exp。
  2. 把 exp 转成可读日期。
  3. 用服务端时间比较,不要只用本机时间。
  4. 检查签发方使用的是秒还是毫秒。
  5. 检查 access token 的有效期是否符合客户端预期。
const payload = decodePart(token.split(".")[1]);
const now = Math.floor(Date.now() / 1000);
console.log({
  exp: payload.exp,
  expDate: new Date(payload.exp * 1000).toISOString(),
  now,
  nowDate: new Date(now * 1000).toISOString(),
  secondsRemaining: payload.exp - now
});

如果 Token 确实过期了,正确做法通常是完善 refresh 流程,而不是把有效期无限延长。长期有效的 access token 一旦泄露,影响面更大。

不同库的报错形状能告诉你卡在哪一层。jsonwebtoken 抛 TokenExpiredError,带 expiredAt 日期;jose 抛 JWTExpired,code 是 ERR_JWT_EXPIRED;OAuth 资源服务器通常返回 401 和 WWW-Authenticate: Bearer error="invalid_token",这是 HTTP 错误,不是解析异常。

不用登录服务器也能对时:curl -sI https://api.example.com/health 返回的 Date 头由服务端生成,如果比你本机快几分钟,多半是校验方时钟有问题,Token 本身没坏。位数也能快速判断单位:当前秒级时间戳是 10 位(一直用到 2286 年),毫秒级是 13 位。看到 13 位的 exp,基本可以确定签发代码忘了除以 1000。

错误二:jwt not active 或 TokenNotYetValid

nbf 在未来,或者 iat 晚于校验方当前时间。常见原因:

  • 签发方时钟比校验方快。
  • Token 被设置为未来某个时间才生效。
  • 测试数据使用了硬编码的未来日期。

比较 nbf、iat 和当前时间。如果只差几秒,应该在校验库中配置一个小的时钟容差,而不是删掉声明。

边界是半开区间:当前时间正好等于 nbf 时有效,正好等于 exp 时无效。有的库用 > 比较,差一秒的差别在任何容差面前都没有意义,真正要关注的是分钟级偏差。

另一个典型场景是虚拟机从快照恢复,或者宿主机时钟源异常。某台 Pod 快 5 分钟,它签发的 Token 在集群其他节点看来 5 分钟内全部“尚未生效”,然后故障自己消失。这种自愈模式基本可以断定是时钟漂移,而不是代码缺陷。

错误三:invalid signature

签名校验失败。可能是 Token 过期后被重新签发、使用了不同的密钥、签发方用非对称密钥而校验方期待共享密钥,或者 Token 在传输中被修改。

先看 header:

{
  "alg": "HS256",
  "typ": "JWT"
}

然后确认:

  • 校验方和签发方使用相同算法。
  • 密钥或公钥与环境匹配(开发环境和生产环境不能混用)。
  • Token 没有被代理或日志系统截断。
  • 密钥轮换后校验方已经更新。

不要通过关闭验签来“修复”签名错误。那等于把安全控制变成摆设。

签名失败有很多是环境问题。用 JWT_SECRET=$(cat secret.txt) 读密钥会把文件末尾的换行一起带进来,签发方如果没带换行,HMAC 输入就差一个字节,所有签名都对不上。两边都打印密钥的字节长度即可确认。密钥轮换也是高频原因:签发方新增密钥后,Token 会带 kid 头;校验方如果忽略 kid、永远用第一把密钥,新密钥签发的 Token 全部失败。正确做法是从 JWKS 端点取密钥,按 kid 选择,遇到未知 kid 时刷新缓存,并限制刷新频率,避免伪造 kid 引发请求风暴。

对称签名调试时还要记住,签名输入是 base64url(header) + "." + base64url(payload) 的原始字节,不是解码后的 JSON。重新序列化 payload 会改变空白或键顺序,声明看起来一样,签名却不同。

错误四:invalid algorithm 或算法混淆

alg 告诉校验方使用哪种算法。接受 Token 自己声明的任意算法非常危险。校验方应该设置算法白名单:

jwt.verify(token, key, { algorithms: ["RS256"] });

这样可以防止攻击者把 alg 改成 none,或者在对称和非对称算法之间切换。RFC 8725 也明确建议使用算法白名单并校验密钥类型。

还有一种情况:拿到的根本不是 JWT。JWE 由五段组成,第二段是加密密钥,不是 JSON payload,按 JWT 的方式解码会在 Base64URL 或 JSON 解析阶段失败。解码前先数一下段数。非对称算法还必须和密钥类型匹配:RSA 公钥验不了 ES256,EC 密钥也验不了 RS256。JWKS 里常常有多把密钥,只按 kid 选还不够,应该同时匹配 alg,否则类型不匹配会被报成签名错误,把你引向不存在的密钥问题。

时钟偏移

分布式系统很难做到时钟完全一致。允许一个小的容差:

jwt.verify(token, key, {
  algorithms: ["RS256"],
  clockTolerance: 60
});

60 秒是一个合理起点。如果经常需要几分钟的容差,应该去修时钟同步,而不是继续放大容差。容差越大,过期 Token 被接受的窗口越长。

容差应该测量后再定。Linux 上用 timedatectl 或 chronyc tracking 看当前偏移和 NTP 是否同步;正常的云主机偏移在毫秒级,NTP 被封锁的机器可能每天漂移数秒。按观察到的最大值来设,不要照抄一个整数。安全成本也要算清楚:120 秒容差意味着泄露的 Token 在 exp 之后还能多用两分钟,5 分钟有效期的 Token 实际上 7 分钟内都可用。容差相对有效期不能太大,偏移开始增长时应该告警。

Access Token 与 Refresh Token

常见设计是:

  • 短期 access token,通常 5 到 15 分钟。
  • 长期 refresh token,通常几天到几周。
  • refresh 接口校验 refresh token 后签发新的 access token。

access token 过期时,客户端应该刷新,而不是强制重新登录。refresh token 需要更小心地存储、轮换,并支持撤销。access token 过期不是安全事故;永久有效的 refresh token 才是更大的风险。

并发场景下刷新流程有一种典型故障:多个标签页或并行请求同时发现过期,一起调用 refresh 接口;轮换机制在第一次成功后作废了 refresh token,其余请求拿到 invalid_grant,用户被登出。客户端应该把刷新合并成同一个 in-flight Promise,或者在有效期到 80% 左右主动刷新,降低碰撞概率。服务端做轮换时最好加复用检测:只保存当前 refresh token 的哈希,兑换后标记已用,旧 token 再次出现就撤销整个 token family。这样被盗的 refresh token 会从“永久访问权”变成可检测事件。

JWT 也不是所有场景都合适。服务端 session 可以立即撤销,不需要黑名单;自包含 JWT 在 exp 之前一直有效,除非额外维护撤销列表。如果即时撤销是硬需求,要么把 access token 做得非常短,要么改用配合会话存储的不透明 token。

实用排查清单

  1. 确认 Token 由三段组成。
  2. 解码 header 和 payload,但不验签。
  3. 读取 alg、exp、iat、nbf。
  4. 转成日期,和服务端时间比较。
  5. 确认预期算法和密钥。
  6. 用生产环境相同的库和配置验签。
  7. 检查代理、网关或日志是否修改了 Token。
  8. 用最小请求复现问题。
  9. 数一下段数:三段是 JWT,五段是 JWE,处理方式完全不同。
  10. 用签发方的 Date 响应头和 iat 对比校验方时钟。

调试对称签名时,可以用 JWT 签发工具 生成已知声明的测试 Token,用 HMAC 工具 查看签名输入。测试密钥不要进入生产环境,也不要写进日志。

不要做的事

  • 不要把密钥放进 JWT payload。
  • 不要在生产环境使用 alg: none。
  • 不要因为能解码就信任 Token。
  • 不要用过长的有效期掩盖 refresh 逻辑的问题。
  • 不要记录完整 Token;记录短指纹和必要声明即可。
  • 能用 HttpOnly、Secure、SameSite 的 Cookie 承载会话时,不要把长期 Token 放进 localStorage,任何 XSS 都能读走它。
  • 不要把 typ 头当作安全控制。它是可选的,经常缺失,也能被随意修改。

过期既是可靠性问题,也是安全边界。把时间戳当作需要检查的数据,把签名当作信任这些数据的唯一理由。

常见问题

exp、iat、nbf 有什么区别?

exp 是过期时间,iat 是签发时间,nbf 是生效时间。三者都是以 1970-01-01T00:00:00Z 为起点的秒级 Unix 时间戳。只有当前时间在 nbf 之后、exp 之前时,Token 才有效。

为什么刚生成的 JWT 就显示过期?

最常见的原因是用毫秒而不是秒设置 exp、有效期设置得太短,或者服务端时钟比客户端快。解码 payload,把 exp 和当前秒级时间戳比较即可确认。

解码 JWT 后可以直接相信里面的声明吗?

不可以。解码只是把 header 和 payload 做 Base64URL 解码,任何人都能修改 payload 后重新编码。必须先使用正确的密钥和算法验签,才能把声明用于鉴权。

时钟偏移应该允许多少?

分布式系统通常允许 30 到 120 秒。选择能覆盖实际时钟漂移的最小值,并监控漂移情况,而不是无限放大容差。

JWT 过期是否意味着用户必须重新登录?

不一定。很多系统使用短期 access token 加长期 refresh token。access token 过期后,只要 refresh token 仍然有效且未被撤销,就可以换取新的 access token,而不必强制用户重新登录。

参考资料

把指南用起来

浏览全部工具

打开浏览器工具。输入内容保留在浏览器中,不会上传到业务服务器。

相关指南

全部指南