Token过期判断与自动刷新:从JWT到双Token机制全解析
2026/8/30 2:14:38 网站建设 项目流程

面试里问到 Token,很多人能背出 JWT 三个字,但真正拉开差距的,是两个具体问题:怎么判断 Token 是否过期,以及自动更新 Token 要怎么实现。这两个问题既是软件测试面试的必问题,也是真实项目里最容易踩坑的地方。很多候选人把过期判断答成“看 exp 字段就够了”,把自动更新答成“前端定时刷新”,一听就知道没碰过完整项目。这篇文章按实际项目里落地的方式拆一遍,附带测试用例设计和面试答题思路。适合准备软件测试面试的同学,也适合刚接手接口鉴权模块的开发或测试。

1. Token 为什么会过期:把超时机制和失效机制分开理解

1.1 过期是时间问题,失效是状态问题

判断 Token 是否过期之前,先搞清楚一件事:过期和失效不是同一个概念。

过期是一个时间概念。服务端在签发 Token 时给它设置一个有效期,常见做法是 JWT 里放一个 exp(expiration time)字段,单位是 Unix 时间戳。有些 Token 还会带 iat 和 nbf,分别表示签发时间和生效时间。如果当前时间已经大于 exp,Token 从时间上就已经过期了。

失效是一个状态概念。Token 可能还没到过期时间,但服务端已经让它不可用了。比如:

  • 用户修改了密码,系统把旧 Token 全部拉黑。
  • 管理员封禁了账号,会话被强制终止。
  • 用户主动退出登录,前端删掉本地 Token,服务端把 jti(Token 唯一标识)写入黑名单。
  • 单点登录场景下,同一账号在另一个设备登录,旧设备 Token 被踢下线。

有一类常见面试追问就在这里:前端拿到的 Token 明明还没到过期时间,为什么接口返回 401?真实原因往往不是过期,而是服务端不再承认这个 Token。测试人员在设计用例时,要同时覆盖时间过期和服务端失效两条链路。

1.2 为什么不能把 Token 时间设得足够长

有人会想,既然判断过期和自动更新都麻烦,那把 Access Token 的有效期设成 30 天甚至一年,不就没这个问题了?

从安全角度看,这不可取。Token 一旦泄露,泄露窗口时间越长,攻击者能盗用的时间就越长。权限变更后,旧的 Token 如果长时间有效,用户改了权限也无法即时生效。用户要求强制下线时,服务端也难以及时把旧 Token 全部吊销。

所以业界通用的思路是:短期令牌负责访问,长期令牌负责续期。Access Token 有效期短,通常几分钟到几小时;Refresh Token 有效期长,通常几天到一个月。Access Token 过期后,客户端用 Refresh Token 换新的 Access Token。这就是自动更新 Token 的基础设计。

2. 判断 Token 是否过期:前端预判、服务端终判、测试验证

2.1 JWT 本地解码怎么判断

JWT 的 payload 是 Base64 编码,前端可以在不请求服务端的情况下解码出来。下面是一个手动解码的示例,用于测试时可以快速看 exp:

import base64 import json def decode_jwt_payload(token): parts = token.split(".") if len(parts) < 2: return {} # JWT 的 payload 部分使用 Base64URL 编码,长度可能不足 4 的倍数 padding = "=" * (4 - len(parts[1]) % 4) payload = base64.urlsafe_b64decode(parts[1] + padding) return json.loads(payload) payload = decode_jwt_payload( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiemhhbmciLCJleHAiOjE3MTY2MjQwMDB9.signature" ) print(payload.get("exp"))

也可以借助 PyJWT 这类常见库,只解 payload 不验签:

import jwt payload = jwt.decode(token, options={"verify_signature": False}) print(payload.get("exp"))

但这里要明确一个边界:本地解码只能做提前预判,不能作为安全结论。原因很简单:

  • 前端时间可能不准。本地电脑时间被改错、没有自动同步,都会导致判断偏差。
  • JWT 只是 Token 的一种。很多项目的 Access Token 是随机字符串,本地根本解不出 exp。
  • 服务端可能维护黑名单、踢人逻辑、权限变更记录,这些状态本地并不知晓。

所以前端最多拿 exp 做“提前提醒登录状态即将失效”之类的 UI 优化,真正的过期判断必须以后端接口返回为准。

2.2 服务端到底校验什么

服务端收到请求后,通常会做几件事:

  1. 校验签名,确认 Token 是服务端自己签发的,没有被篡改。
  2. 校验时间字段,确认当前时间是否在 nbf 和 exp 之间。
  3. 校验 iss、aud 等声明,确认 Token 的签发方和受众是否匹配。
  4. 查询黑名单或 Redis 登录态,确认 Token 是否已经被吊销。
  5. 结合业务场景校验账号状态,比如是否被封禁、是否被踢下线。

用一个通用伪代码表示大概是这样:

def verify_token(token): try: payload = jwt.decode(token, public_key, algorithms=["RS256"]) except jwt.ExpiredSignatureError: raise TokenExpiredError("expired") except jwt.InvalidTokenError: raise TokenInvalidError("invalid") # 有些项目会额外检查 jti 是否在黑名单里 if redis.exists(f"blacklist:{payload['jti']}"): raise TokenRevokedError("revoked") return payload

这个流程说明一个问题:前端看到“Token 没过期”,服务端却可能因为黑名单、账号状态等原因拒绝。测试人员排查接口返回 401 时,不能只看时间,还要看服务端日志里具体是哪种错误。

2.3 用 HTTP 状态码和错误信息定位过期还是失效

判断 Token 是否过期,最直接的痕迹来自接口响应。常见状态码:

  • 401 Unauthorized:未携带 Token、Token 过期、Token 非法、Token 被吊销,都可能返回 401。
  • 403 Forbidden:Token 本身有效,但没有权限访问这个资源。
  • 400 Bad Request:请求参数或请求头格式不正确,比如 Authorization 头没有带 Bearer 前缀。

遇到 401 时,优先看响应头和响应体。很多服务端会在响应头里写:

WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"

响应体里也可能有 error_code 或 message,比如:

{ "code": 40101, "message": "token expired" }

这里要提醒一个容易误判的场景:如果看到类似token exchange failed: token endpoint returned status 403 forbidden: country, region, or territory not supported的报错,这类问题通常是账号归属地与当前访问区域不一致,或者是网络出口地址发生了变化,属于访问控制问题,不是 Token 过期。测试时不要把这类问题当成刷新逻辑的 bug,要按网络和账号方向排查。

3. 自动更新 Token:双 Token 机制和刷新链路

3.1 Access Token 和 Refresh Token 怎么分工

自动更新 Token 的前提是有一套双 Token 机制。两者的关系用一张表说明:

维度Access TokenRefresh Token
有效期短,常见 15 分钟到 2 小时长,常见 7 天到 30 天
使用场景每次接口请求都携带只用于刷新接口
存储位置前端内存、localStorage、Cookie 等需要更高安全级别,通常放 HttpOnly Cookie 或安全存储
泄露影响短时间内可以冒用身份泄露后可以长期换取新 Access Token
服务端状态JWT 场景下可以无状态通常需要记录、验证、轮换

Refresh Token 的设计原则是“低频使用、高安全要求”。它不应该出现在普通接口请求里,也不应该被业务代码随意读取。

3.2 刷新流程一次走通

自动更新的完整链路可以拆成六步:

  1. 客户端携带 Access Token 发起业务请求。
  2. 服务端校验发现 Access Token 过期,返回 401。
  3. 客户端拦截到 401,不再直接抛给用户,而是先调用刷新接口。
  4. 刷新接口校验 Refresh Token,如果有效就签发新的 Access Token,有些实现还会同时签发新的 Refresh Token。
  5. 客户端保存新 Token。
  6. 客户端重放刚才失败的原始请求。

刷新接口的请求体通常类似于这样,具体字段以项目实际协议为准:

{ "refreshToken": "xxxxxxxx", "grantType": "refresh_token" }

刷新接口的响应通常长这样:

{ "accessToken": "new-access-token", "refreshToken": "new-refresh-token", "expiresIn": 3600 }

这里最重要的一点是:自动刷新不能散落在每个业务请求里自己写,应该由统一的 HTTP 拦截器处理。前端可以在 axios 拦截器、后端可以写中间件,统一识别 401,统一调用刷新逻辑,统一重放请求。否则写十个接口就有十份重复代码,出了问题非常难排查。

3.3 Refresh Token 的安全存储和轮换

Refresh Token 比 Access Token 更敏感,存储位置要单独考虑。放在 localStorage 里容易被 XSS 脚本读取,放进 HttpOnly Cookie 能降低 XSS 风险,但要注意 CSRF 防护。放到前端内存里则刷新页面后丢失,用户会频繁重新登录。每个方案都有取舍,面试时可以按项目场景说明。

更关键的是 Refresh Token 轮换。每次刷新成功后,服务端应该签发一个新的 Refresh Token,同时让旧 Refresh Token 失效。这样即使旧 Refresh Token 在某个环节泄露,攻击者也没法长期复用。

服务端还要做重用检测:如果发现一个已经被轮换过的旧 Refresh Token 再次出现在刷新请求里,说明这个 Token 可能已经泄露,应该把这个会话下的所有 Token 全部作废,强制用户重新登录。

4. 并发刷新和请求重放:最容易翻车的地方

4.1 多个 401 同时打过来会发生什么

假设用户在页面上停了一段时间,Access Token 已经过期。此时用户突然点击某个操作,前端一下子发出五个请求。这五个请求都会收到 401。如果每个请求都各自去调刷新接口,会发生两个问题:

一是刷新请求被调用五次,浪费网络资源。二是如果 Refresh Token 每次刷新都会轮换,五次刷新里只有第一次能成功,后面四次可能拿到已经失效的旧 Refresh Token,直接导致用户被强制下线。

这个坑在真实项目里太常见了。所以自动更新 Token 不仅仅是一个接口逻辑,更是一个并发控制问题。

4.2 用一个刷新标志把并发请求挂起来

正确做法是:第一次收到 401 时,开始刷新,后续请求进入待处理队列,等刷新完成后再批量重放。

用一个前端伪代码说明思路:

let isRefreshing = false; let pendingQueue = []; async function requestWithToken(config) { const res = await doRequest(config); if (res.status !== 401) { return res; } // 已经有刷新任务在进行,暂时挂起 if (isRefreshing) { return new Promise((resolve, reject) => { pendingQueue.push({ config, resolve, reject }); }); } isRefreshing = true; try { const newToken = await refreshToken(); // 只调用一次刷新接口 updateToken(newToken); // 重放等待中的请求 pendingQueue.forEach((item) => { item.resolve(doRequest(item.config)); }); pendingQueue = []; // 重放当前请求 return doRequest(config); } catch (e) { // 刷新失败,所有等待中的请求一起失败 pendingQueue.forEach((item) => { item.reject(e); }); pendingQueue = []; redirectToLogin(); throw e; } finally { isRefreshing = false; } }

这个思路在不同语言里都可以套用。后端中间件也可以用一个全局刷新状态和一个等待队列,实现类似的效果。重点不是代码抄不抄,而是要让并发请求统一等待同一个刷新结果。

4.3 重放请求要保留完整上下文

重放请求不是简单再发一次。原请求里的 method、url、headers、body、超时时间、取消信号都要一起保留。有些 body 类型只能读取一次,比如流式请求或 FormData,如果第一次发送时读过了,重放时可能拿不到数据。这种情况下要在请求发出前先缓存一份副本。

另外,重放次数要有限制。正常情况一次 401 触发一次刷新,刷新成功重放一次就够了。如果重放后仍然 401,说明新的 Access Token 可能也有问题,或者权限已经被收回,这时候要直接失败,不能进入无限重试循环。

5. 测试人员怎么设计 Token 场景用例

5.1 从正常链路到异常链路的用例清单

软件测试面试问到 Token,本质上是在问你能不能把异常场景设计完整。下面是一份可以直接参考的用例清单:

用例场景操作方式预期结果
Access Token 未过期正常登录后立即请求接口请求成功,不出现刷新请求
Access Token 已过期等待 Token 过期或手动构造过期 Token返回 401,自动触发刷新,原请求重放成功
Refresh Token 已过期使用过期 Refresh Token 刷新刷新失败,跳转登录页
刷新期间多个请求并发同时发起多个接口请求只调用一次刷新接口,所有请求最终成功
刷新期间新请求进入刷新尚未完成时再发新请求新请求排队,刷新完成后继续
刷新接口网络异常断网或制造超时不无限重试,提示用户,保留登录态或跳登录
旧 Refresh Token 被再次使用刷新成功后使用旧 Refresh Token服务端拒绝,视安全策略决定是否踢登录
多端登录同一账号两个设备同时使用不互相干扰,或按业务规则强制下线
服务端时间偏差修改服务端时间造成偏移能容忍短时误差,不误杀正常请求
权限变更后管理员修改用户角色后请求接口新权限立即生效或按策略过期生效

这些用例能覆盖自动更新 Token 的主要风险点。面试时能说出其中五六个,已经能证明你理解的不只是概念。

5.2 怎么在测试环境模拟 Token 过期

测试环境模拟过期,有几个常见方法:

  • 后端配置短 Token 有效期,比如把 Access Token 设成 60 秒,方便快速验证。
  • 用接口工具生成一个 exp 已经过去的 JWT,替换到请求头里。
  • 在登录态存储里把当前用户的会话删除,模拟服务端失效。
  • 如果系统支持,直接调用内部接口或数据库操作把 Token 加入黑名单。

有一点容易踩坑:如果不调整服务端时间,手动改 JWT 的 exp 也不一定有效,因为服务端校验用的是服务器时间。反过来,如果测试时发现“Token 明明没过期,接口却说过期”,先看服务端时间和本地时间是否一致。很多项目在虚拟机、容器环境里时间漂移,会导致 Token 校验结果不稳定。测试机器最好开启时间自动同步,服务端则要单独确认 NTP 配置。

5.3 用日志和抓包判断刷新是否成功

判断自动更新是否生效,不能只看最终接口成功没有,还要看请求链路。用接口测试工具或抓包工具观察:

  1. 第一个业务请求返回 401。
  2. 紧接着出现一个刷新请求,请求体里带 refreshToken。
  3. 刷新请求返回 200,响应体里有新的 accessToken。
  4. 原来的业务请求被重放,这次返回 200。

如果看到 401 后没有刷新请求,说明前端没有拦截或没有走统一刷新逻辑。如果看到多个刷新请求,说明并发控制没有生效。如果刷新请求成功但业务请求没有重放,说明拦截器拿到了新 Token 之后没有继续执行原请求。

服务端日志要关注这几个关键词:token expired、refresh token invalid、refresh success、token rotated。配合请求日志,基本可以定位是前端问题、后端问题还是网络问题。

6. 面试答题参考和常见追问

6.1 先答清楚两层判断

面试官问“如何判断 Token 是否过期”时,一个稳妥的回答顺序是:

先回答本地层:JWT 可以在前端解码看 exp,不透明 Token 则无法本地判断。但本地判断只能做提前预警,不能作为最终结论,因为服务端可能还有黑名单和状态校验。

再回答服务端层:最终以服务端校验为准。服务端会验证签名、时间、jti、黑名单和账号状态。过期时返回 401,无效或吊销也返回 401,但响应头和响应体会带有具体错误信息。

这样回答,面试官能听出你是从实际项目出发,而不是背了一个“看 exp”的结论。

6.2 自动更新 Token 的标准回答骨架

自动更新 Token 可以按四层来说:

第一层是双 Token 机制:Access Token 负责请求,Refresh Token 负责续期。Access Token 设置短有效期,Refresh Token 设置长有效期,刷新成功后进行 Refresh Token 轮换。

第二层是统一拦截:通过 HTTP 拦截器或中间件统一监听 401,不让业务代码各自处理。

第三层是并发控制:刷新过程中使用队列或复用同一个刷新 Promise,避免多个请求重复刷新。

第四层是失败兜底:刷新接口返回 401、超时或网络异常时,清除登录态,跳转登录页,设置最大重试次数,不无限循环。

这套骨架覆盖了机制、实现、并发、安全四个维度,面试时可以按这个顺序展开。

6.3 常见追问和应对思路

追问一:前端定时器判断 Token 过期可以吗?

可以,但只能做 UI 层的提前感知,不能替代服务端校验。比如提前一分钟提示用户登录即将失效。而且用户在后台标签页时,定时器可能被浏览器节流或挂起,所以最终还是要靠 401 触发刷新。

追问二:如果 Refresh Token 也被盗了怎么办?

从安全策略上应对:Refresh Token 轮换,旧 Token 重用即视为异常;绑定设备信息或指纹;检测到异常 IP 后要求重新登录;缩短 Refresh Token 有效期,设置合理的安全阈值。

追问三:刷新接口本身也返回 401 怎么办?

说明 Refresh Token 已经无效或者被轮换。此时不应继续重试,应该清理本地登录态,引导用户回到登录页。这里还要注意,刷新接口返回的 401 不能被拦截器再次触发刷新,否则会死循环,通常需要给刷新接口单独设置一个跳过逻辑。

追问四:后端如何实现双 Token 发布?

登录成功后签发两个 Token,Access Token 短时间有效,Refresh Token 保存到 Redis 或数据库,并记录过期时间。收到刷新请求后,先从存储里查 Refresh Token,校验成功后签发新的 Access Token 和新的 Refresh Token,同时删除旧记录。如果 Refresh Token 在黑名单里,说明可能被重用,需要做安全处理。

追问五:测试时怎么验证自动刷新只调用了一次?

可以做并发接口测试:同时发送五个请求,观察抓包结果,刷新接口只能出现一次。如果出现多次,说明并发控制有问题。这个用例在真实项目里非常值得写进回归测试。

自动更新 Token 这套链路,真正落地时最该盯住的不是概念,而是并发刷新、刷新失败兜底和 Refresh Token 轮换。面试时能把判断逻辑说清楚是基础,能把并发刷新和失败处理讲明白才是加分项。如果正在准备面试,建议自己动手画一遍请求时序图,再用接口工具模拟一次过期 Token,很快就能形成自己的回答框架。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询