十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Token过期判断与自动更新:从原理到工程化实践

Token过期判断与自动更新:从原理到工程化实践 面试时被问到“如何判断 Token 是否过期自动更新 Token 要如何实现”很多人第一反应是这个我会解析 JWT 里的exp字段过期就调刷新接口。但面试官只要继续追一句“如果exp还有 5 分钟但服务端已经封禁了这个用户你怎么办”“如果同时有五个请求都返回 401会不会触发五次刷新”“刷新接口本身也返回 401你是不是会陷入死循环”——不少人就卡住了。这道题看起来在问 Token实际上问的是整套认证流程的闭环能力。它不是三个孤立的八股知识点而是一条从登录到请求、从过期到刷新、从成功到失败的完整链路。答得好不好直接暴露你是背过概念还是真的在项目里处理过会话管理。这篇文章不打算只给你一份“面试标准答案”。我把问题拆开从 Token 为什么过期讲起到前端怎么判断、后端怎么校验、刷新链路怎么设计、并发请求怎么兜底再到工程化落地时容易踩的坑和完整的排查顺序。你可以在下次面试前用它整理思路也可以直接把它当成项目里实现 Token 自动续期的设计参考。1. 面试官问“如何判断 Token 是否过期”到底在考察什么1.1 这个问题背后不是三个知识点而是一条链路很多测试岗和前端岗候选人看到这道题第一反应是把它拆成三个名词解释Token、过期、自动更新。于是答案变成了“Token 是凭证有过期时间过期后重新登录或者用 Refresh Token 刷新”。这个回答不能算错但它停在表面。面试官真正想听的不是名词解释而是你如何把一次登录后的所有请求都放进一个可控流程里。考察点通常有三个层次第一层知不知道 Token 有过期机制用什么字段表示过期。第二层能不能设计一段可执行的判断逻辑而不是只会背 JWT 的exp。第三层能不能把“请求发出—服务端拒绝—前端刷新—重放请求—刷新失败”这条完整链路讲清楚并指出链路里最容易出问题的几个环节。第三层才是拉开差距的地方。因为前两层是记忆问题第三层是设计问题。1.2 为什么 Token 必须有过期时间大部分 Token尤其是 JWT并不在服务端保存状态服务端只需要用密钥验签就能确认这个 Token 是否有效。但“能验签”不等于“永远有效”。如果 Token 永远不过期会出现三个实际风险泄露后的窗口期无限拉长一个被盗的 Token 可以一直被使用。用户改密码或账号被封禁后旧 Token 仍然能通过签名校验服务端要额外加黑名单才能拦截。权限变更无法及时生效例如用户被降权已签发的 Token 里的角色信息还是旧值。所以 Token 过期不是一种缺陷而是一种安全策略。常见实现里有三种失效方式失效方式含义典型场景绝对过期Token 从签发时刻算起超过指定时间就失效短期 Access Token例如 30 分钟滑动过期每次有效操作后重新计算过期时间例如无操作 2 小时后失效网页端登录态用户连续操作不退出主动吊销服务端将某个 Token 或对应用户标记为失效改密码、退出登录、封禁用户面试里最常讨论的是“绝对过期 主动吊销”的组合Access Token 短时间过期Refresh Token 长时间有效必要时服务端主动吊销用户的所有会话。理解了这个组合你才能理解为什么前端不能只靠本地exp判断。这里有个很容易混淆的点JWT 里虽然有exp但它只是一个“声明”不是执行限制。真正限制请求是否通过取决于服务端是否校验exp以及是否额外检查吊销状态。前端本地读exp只是做体验优化不是安全校验。2. 判断 Token 是否过期前端拦截、接口响应和兜底策略2.1 前端可以读取 exp但不要把它当成唯一依据Token 是 JWT 格式时它的第二部分 Payload 是一个 Base64 编码的 JSON里面通常有exp、iat、sub等字段。前端可以用jwt-decode一类库解析拿到exp后和当前时间比较import { jwtDecode } from jwt-decode function isTokenExpired(token) { if (!token) return true try { const { exp } jwtDecode(token) if (!exp) return false return Date.now() exp * 1000 } catch (e) { return true } }这份代码可以出现在你的项目里也可以在面试时写出来。但它解决的是“本地自测”问题不是“服务端是否接受”问题。原因有四个时钟偏差用户手机时间不对本地判定还能用服务端已经判死。服务端主动吊销管理员封了用户、改密、踢人下线exp还很远但 Token 已经无效。Token 内容被篡改签名校验在前端读不到只能等服务端返回 401。网络层问题有时候不是 Token 过期而是接口因为权限变更返回 401本地exp完全无法感知。所以在真实项目里前端一般会做“两层判断”先用本地exp做预判提前处理明显要过期的请求减少一次无意义的网络请求再用服务端返回的 401 作为最终兜底。两边的职责不一样一个是减少问题一个是发现结果。2.2 后端什么情况下返回 401什么情况下返回 403这个点经常被面试官追问也和 Token 判断直接相关。很多人把 401 和 403 混着用导致前端拦截逻辑做不对。401 Unauthorized请求缺少凭证或凭证非法、过期、无法通过校验。意思是“我没有确认你是谁”。403 Forbidden凭证有效但你无权访问这个资源。意思是“我知道你是谁但你没权限”。Token 过期后服务端应该返回 401因为签名合法但生命周期结束。用户 Token 有效但访问了超出权限的接口应该返回 403。前端拦截器如果只按状态码处理就必须区分这两者否则会把所有 403 都当成“登录过期”去触发刷新反而进入了错误分支。在实际处理时更推荐后端不仅返回状态码还在响应体里给一个明确错误码例如INVALID_TOKEN、TOKEN_EXPIRED、PERMISSION_DENIED。前端优先根据错误码判断状态码作为辅助。2.3 一个可复用的 Token 过期判断流程把前后端逻辑合起来看一个最小可复用流程是这样请求发出前前端读取本地 Token解析exp。如果已经过期或接近过期先尝试刷新刷新成功后再发请求。如果exp还有效正常发请求。服务端校验签名、过期时间和吊销状态。如果校验失败返回 401 和业务错误码。前端收到 401判断是否已经重试过没有重试过就触发刷新成功后重放原请求。如果刷新失败比如 Refresh Token 也过期了清除本地登录态跳转登录页。这个流程在面试时讲清楚面试官基本就能确认你不是只会背概念而是真的设计过会话流程。真正复杂的部分在第六步刷新链路怎么设计才能避免并发风暴和死循环。3. 自动更新 Token刷新链路设计才是核心3.1 为什么要用双 Token短期凭证 长期凭证如果把 Access Token 设置为半小时过期用户每半小时就要重新登录一次体验很差。如果设置为三十天过期泄露后的风险窗口又太长。于是常见的做法是引入双 TokenAccess Token有效期短例如 30 分钟用于访问业务接口。Refresh Token有效期长例如 7 天或 30 天只用于换取新的 Access Token不用于访问业务接口。Refresh Token 相当于一个“重新登录的凭证”。它让用户不需要频繁输入账号密码但业务接口使用的凭证仍然保持短期有效降低泄露影响。双 Token 不是唯一方案也有项目用滑动过期来解决“活跃用户长期不用重新登录”的问题。但双 Token 是面试和项目里最常见的设计因为它把“短期安全”和“长期体验”分开处理职责更清晰。3.2 请求拦截器统一处理 401 和刷新前端实现自动更新 Token最常见的做法是在 HTTP 客户端封装拦截器。以 axios 为例代码结构通常是import axios from axios import { jwtDecode } from jwt-decode // 刷新中的 Promise避免并发重复刷新 let refreshingPromise null // 存储访问凭证 function getAccessToken() { return localStorage.getItem(access_token) } function getRefreshToken() { return localStorage.getItem(refresh_token) } function saveTokens(accessToken, refreshToken) { localStorage.setItem(access_token, accessToken) localStorage.setItem(refresh_token, refreshToken) } function clearTokens() { localStorage.removeItem(access_token) localStorage.removeItem(refresh_token) } // 每次请求前带上 Access Token axios.interceptors.request.use((config) { const token getAccessToken() if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器统一处理 401 axios.interceptors.response.use( (response) response, async (error) { const { config, response } error // 不是 401直接抛错 if (!response || response.status ! 401) { return Promise.reject(error) } // 已经重试过说明刷新后仍失败跳转登录 if (config._retry) { clearTokens() redirectToLogin() return Promise.reject(error) } config._retry true try { await refreshAccessToken() // 刷新完成后重新发起原请求 config.headers.Authorization Bearer ${getAccessToken()} return axios(config) } catch (refreshError) { clearTokens() redirectToLogin() return Promise.reject(refreshError) } } ) async function refreshAccessToken() { // 已经有刷新请求在进行了直接复用同一个 Promise if (refreshingPromise) { return refreshingPromise } refreshingPromise axios .post(/api/auth/refresh, { refresh_token: getRefreshToken(), }) .then((res) { saveTokens(res.data.access_token, res.data.refresh_token) }) .finally(() { refreshingPromise null }) return refreshingPromise }这段代码的逻辑很简单所有请求都带Authorization头。请求返回 401 时先检查这个请求是否已经重试过。没重试过调用refreshAccessToken。刷新成功用新 Token 重放原请求。刷新失败清除本地身份信息跳登录页。面试时能画出这张流程图比背概念更有说服力。3.3 并发请求下的刷新竞态不能每个 401 都触发刷新这一步最容易暴露真实项目经验。假设页面同时发出 6 个业务请求Access Token 恰好过期了它们会几乎同时收到 401。如果你的拦截器写得简单每个 401 回调里都调一次刷新接口就会把一次刷新变成六次并发刷新。更好的做法是把刷新接口的调用做成单例第一次触发时创建一个 Promise把它存到模块级变量里后续请求不重新创建而是共用这个 Promise。代码里refreshingPromise就是干这个事的。这里有几个细节要注意刷新完成后要重置refreshingPromise否则下一次过期无法触发刷新。刷新失败时同样要重置不然失败状态会被一直缓存。重放请求时要确保用最新的 Access Token而不是在 401 发生时取到的旧 Token。如果 Token 是存在内存里刷新成功后还要考虑多个标签页之间如何共享。这是一个很深的工程问题但面试时提出来是加分项。3.4 后端刷新接口怎么设计刷新接口看起来只是“拿着 Refresh Token 换新 Access Token”但真正落地时要考虑三点。第一Refresh Token 是否只能用一次。推荐做法是轮换每次刷新成功后旧的 Refresh Token 作废服务端返回新的 Refresh Token。这样即使 Refresh Token 泄露攻击者用一次后原用户的下一次刷新也会把泄露的 Token 顶掉。第二服务端要有 Refresh Token 的存储和吊销标记。它不一定非要存数据库但至少要能在用户改密、封禁、退出时把相关 Token 标记为无效。第三刷新接口本身也要防重放和防灾难。比如检查 Refresh Token 是否过期、是否吊销、是否属于当前用户、是否在可信设备上。后端接口的简化伪代码可以这样写// 伪代码不绑定具体框架 async function refreshToken(req, res) { const { refresh_token } req.body const record await tokenStore.findByRefreshToken(refresh_token) if (!record) { return res.status(401).json({ code: INVALID_REFRESH_TOKEN }) } if (record.expiresAt Date.now()) { return res.status(401).json({ code: REFRESH_TOKEN_EXPIRED }) } if (record.revoked) { return res.status(401).json({ code: REFRESH_TOKEN_REVOKED }) } const newAccessToken signAccessToken(record.userId) const newRefreshToken generateRefreshToken() // 旧 Refresh Token 作废换新 await tokenStore.revoke(record.id) await tokenStore.save({ userId: record.userId, refreshToken: newRefreshToken, expiresAt: Date.now() 30 * 24 * 60 * 60 * 1000, }) res.json({ access_token: newAccessToken, refresh_token: newRefreshToken }) }这里最核心的设计判断是Access Token 短一点没关系但 Refresh Token 的“存储、吊销、轮换”决定了整个会话体系的边界。面试时说到这一层基本就是高分答案了。4. 从面试到落地工程化要考虑的边界和坑4.1 Token 存哪里不只是技术问题前端存储方式通常是面试里容易聊岔的话题。很多人会说“不能用 localStorage有 XSS 风险”然后被追问“那用什么”又说不上来。实际操作中确实需要区分场景纯前端 SPA常用localStorage或sessionStorage实现简单但要靠编码规范和 CSP 防 XSS。安全要求更高可以用内存变量存储 Access Token刷新后重新获取但刷新页面会让登录态丢失体验变差。刷新 Token 的持久化可以放在 HttpOnly Cookie 里JS 访问不到降低 XSS 泄露风险但要注意 CSRF 防护。没有绝对正确的方案只有取舍。面试时如果能说出“本地存储简单但防不了 XSSHttpOnly Cookie 更安全但要处理 CSRF内存存储最安全但刷新体验差”会比只背一句“用 sessionStorage”效果好很多。4.2 多标签页和跨端的刷新一致性问题自动刷新在单页面里做好到多标签页又会出问题。用户在同一浏览器开了三个标签页Access Token 过期后三个页面同时发请求就可能出现多次刷新。虽然拦截器里的单例 Promise 能兜底但每个标签页各自有各自的 JS 上下文单例只在当前标签页生效。常见的处理思路用BroadcastChannel或storage事件通知其他标签页“Token 已更新”。在刷新 Token 前先检查本地存储里的新 Token 时间戳如果其他标签页已经刷新过直接用新 Token。后端侧支持 Token 族Token Family识别刷新后旧 Refresh Token 如果不能用了其他标签页即使刷失败了也可以靠重新登录恢复。如果只是面试提到“用 storage 事件同步标签页”已经能体现经验。如果在项目里就要根据并发用户和操作频率选择方案不要一开始就上复杂设计。4.3 最小可用方案和生产级方案差在哪不同阶段的项目对 Token 自动更新的要求完全不同维度学习项目 / 内部工具生产系统Access Token 过期时间可以很长或不设短常见 15 分钟到 2 小时刷新方案401 引导重新登录Access Token Refresh Token 自动刷新并发刷新不处理单例 Promise防并发多标签页不处理storage 事件同步或共享刷新状态Refresh Token 存储无或简单存储数据库存储、加密、轮换、吊销吊销能力无改密、封禁、踢人下线时主动吊销审计日志无记录登录、刷新、异常来源如果你正在做一个真实项目我建议从“最小可用”起步先实现 Access Token Refresh Token 401 拦截器把登录、过期、刷新这条主链路跑通再逐步补上并发控制和吊销能力。不要一上来就设计一个包含 Token 族的复杂系统因为很多业务根本用不到但基础链路一旦断裂线上问题会非常难排查。5. 面试容易踩的坑这几个问题必须想明白5.1 常见错误把“刷新 Token”和“换新 Token”混为一谈有些人会答“Token 过期了就调用接口重新换个 Token。”这句话说得没错但没区分“用 Refresh Token 换”和“用旧 Token 续期”是两个不同的实现路径。主流做法是 Refresh Token 换新 Access Token而不是拿旧 Access Token 续期因为旧 Access Token 已经在服务端被判过期了用它续期逻辑上不成立也不够安全。另一种错误是页面里 Token 过期了直接强制用户跳回登录页。这个做法不是错的但对高频用户不友好。如果这是一个内部管理后台可以做如果是面向大量用户的小程序或 App用户会明显感觉到“用着用着就被踢下线”这就需要刷新链路。5.2 常见错误刷新失败后没有退出机制拦截器最怕“死循环”。场景是Access Token 过期拦截器调刷新接口但 Refresh Token 也过期了刷新接口返回 401拦截器看到 401又进了一次刷新逻辑又调刷新接口……如果没有config._retry这类标记请求就会无限重放最终页面卡死。正确做法是两个条件必须同时存在每个请求只能被自动刷新重放一次第二次还是 401就说明刷新链路不可用。刷新接口本身不应该走“响应拦截器里的 401 刷新逻辑”否则刷新失败会再次触发刷新形成嵌套。在代码上可以给刷新接口单独创建实例不带响应拦截器或者直接把_retry标记加在刷新请求上。5.3 常见错误只判断状态码不区分 401 和 403我见过真实项目里后端接口权限不足返回 403前端拦截器误以为 Token 过期于是去刷新刷新成功后重放请求还是 403前端又跳登录页。用户还处于登录状态却被强制退出。回到本文开头的观点前端本地判断只是体验优化服务端校验才是最终标准。但前端也不是只看状态码而是要结合业务错误码区分“凭证失效”“凭证过期”“权限不足”几种情况。尤其是 403通常不应该触发刷新。5.4 一个可用的排查链路如果你在项目里遇到 Token 相关问题不知道怎么定位可以按下面这个顺序排查看现象是某个接口 401还是所有接口 401是刷新接口 401还是业务接口 401看 Token 本身解码后exp是什么时间签名能不能通过校验Payload 里的用户信息是否正常看服务端时间服务器时间和客户端时间是否偏差过大很多 Token 校验问题都出在时钟偏差上。看刷新链路Refresh Token 是否过期是否被吊销服务端是否轮换了 Refresh Token导致旧值不可用看并发是不是多个请求同时触发刷新数据库里刷新 Token 的次数是否异常看用户状态用户是否刚改过密码是否被强制下线是否在别的端登录后导致会话失效排查的顺序原则是先定位是哪一层再看具体数据。不要一上来就怀疑前端拦截器更不要一上来就改 Token 有效期。6. 这类问题真正考核的是什么回到面试场景。面试官问“如何判断 Token 是否过期自动更新 Token 要如何实现”时通常不是真的需要一个标准答案而是在看你有没有能力把这件小事做成一个完整的处理流程。我建议你可以用三层结构来现场组织答案判断层前端解析exp做预判服务端校验签名、过期时间和吊销状态最终以服务端 401 为准。更新层使用双 Token 机制Access Token 短期有效Refresh Token 提供持久凭证前端拦截器统一处理 401 并自动刷新刷新成功后重放原请求。兜底层并发请求要防止重复刷新刷新失败要避免死循环多标签页要处理状态同步服务端要支持吊销和轮换。把这三层讲完你已经不是在背题而是在展示一次“从概念到实现再到异常处理”的完整思考。这比任何标准答案都有说服力。如果这篇文章能给你一个可复用的框架那就是不要只背“如何判断 Token 是否过期”而是把判断、刷新和异常兜底当成一条完整链路。你先设计清楚这条链路里每一步的输入、输出和失败分支再回答面试官的问题就会发现这道题真正考的是你能不能把一个细节问题放进一个更大的系统里思考。
返回列表