新闻详情

新闻详情

首页 / 资讯中心 / 详情

登录状态刷新实战:双token+axios拦截器自动续期,告别401踢出

发布时间:2026/9/28 15:00:04来源:尧图网络
登录状态刷新实战:双token+axios拦截器自动续期,告别401踢出
做了好几年中后台系统每次和“登录状态刷新”问题交手都得掉几根头发。尤其是那种用户正在表单里填了一半资料系统突然弹回登录页一堆未保存数据全没了的情况最让人头大。实际上“状态登录刷新”这个问题的核心不只是“token过期”而是——前端如何预判并自动完成登录态续期同时不打断用户操作以及页面刷新时内存里的登录状态全部清空后如何快速恢复会话。这篇文章就围绕这两个核心展开从问题拆解、双token方案、拦截器统一刷新到多标签页同步把我在真实项目里踩过的坑和验证过可用的做法一次说清楚。适合正在处理登录失效续期、401拦截、多页签同步等需求的同学参考。1. 问题的本质为什么会出现“状态登录刷新”先说一个结论大多数登录态问题都出在“前端把登录状态当成了纯内存数据”或者“把401当成了必须重新登录的信号”。1.1 登录状态失效的几种典型场景我自己整理过一份失效场景清单基本覆盖了日常项目里的绝大多数情况短有效期token到期很多内部系统为了安全把access_token的过期时间设成15分钟或者30分钟。用户习惯一上午不关浏览器期间任何一个接口返回401前端如果处理不当用户就被“静默踢出”。服务端主动吊销会话账号被禁用、送审流程里后台强制改密、管理员在另一端踢人都会让服务端认为当前token失效。页面停留太久后操作早上开的系统、下午回来继续用token已经过期。用户点一个按钮前端收到401。多标签页之间的状态错位一个标签页退出了登录另一个标签页还傻乎乎地带着旧token请求接口。这个场景最隐蔽因为从用户视角来看“我明明登录着”。本地时间与服务器时间不同步我在一个项目里遇到过用户手机时间快了5分钟客户端自己校验token时直接认为过期导致所有请求都带着无效凭证。把这些问题归到根上本质上都是登录凭证存在“生命周期间隙”。就像门锁按时间自动换锁但用户手里的钥匙是旧的前端的任务不是把用户拦在外面而是在换锁那一刻自动把新钥匙递到用户手里。1.2 状态刷新不只是“重新登录”这么简单不少人一开始的做法很直接响应里收到401就调一下“退出登录”然后location.href跳转登录页。这套在内部简单后台还能忍但放到用户量大的产品里完全是灾难——用户正填到一半的工单、刚勾选好的筛选条件、编辑框里没点保存的长文本全没了。而且“刷新登录状态”真正的技术难点在于401可能发生在任意接口你不能在每个业务请求里都写一遍刷新逻辑。多个接口同时返回401时你不能同时发起多个刷新token请求否则后端刷新接口会被打爆。刷新token这个动作本身也可能失败失败之后要怎么兜底是一个独立的判断分支。重放原始请求时必须保证用的是新token而不是被请求拦截器再次塞进旧token。换句话说状态刷新是一个“跨请求的全局协调机制”不是某一个接口里的if分支。2. 主流的登录态管理方案对比在写拦截器之前先花点篇幅对比一下方案。因为拦截器只是“执行层”如果你底层用的还是单token模式那刷新机制根本无从谈起。2.1 单token模式简单直接但续期困难单token模式就是登录成功后后端返回一个token前端每次请求带上token过期就只能重新登录。这个方案在低频率内部工具、后台管理里很常见优点是后端实现简单缺点是用户在线体验极差。我之前维护过一个小型工单系统token有效期设成2小时一个上午就有好几个同事来抱怨“填的内容突然没了”。后来我把过期时间调成一整天虽然抱怨少了但安全上又心虚——员工的token在公共电脑上一天内都能直接复用。单token模式本质上把“安全性和体验”推到对立面你很难两头兼顾。2.2 双token模式刷新登录状态的核心解法双token模式解决了这个矛盾。核心思路是拆成两个凭证access_token有效期短一般15分钟到2小时。专门用来请求业务接口泄露后风险窗口小。refresh_token有效期长7天到30天。只用来换取新的access_token不参与业务接口请求。我把完整流程拆成四步方便你画图理解业务请求发出带上access_token。后端发现access_token过期返回401或您自定义的特定业务码。前端拦截器拦截到401自动用refresh_token调用刷新接口。拿到新access_token后把等待中的原请求重新发出去用户毫无感知。这个模式里其实有个容易忽略的点刷新接口本身也要能被“特别对待”。刷新接口不能再触发刷新逻辑否则会变成递归死循环。所以后面写拦截器时必须给刷新接口单独开一条不受401拦截逻辑控制的通道。2.3 方案选型要看项目场景不能只看“流行”要不要上双token我的建议是看四点用户在线时长几分钟内的纯浏览场景没必要上双token用户一天内长时间挂着的系统一定要上。数据敏感程度涉及资金、生产配置、医疗健康这类数据用短access_token配合refresh_token即使token被截获能造成的破坏也有限。后端改造成本双token需要后端维护refresh_token的存储和失效机制如果后端是外包团队一次性交付建议先评估清楚再上。是否需要支持跨端无缝登录如果产品里已经接了统一身份中心这类服务通常已经管理了会话续期前端要做的工作是“接入”而不是“自建”。我把方案选型整理成一个表方便比照维度单token双token(accessrefresh)实现复杂度低中高用户在线体验过期即踢无感续期安全风险token泄露后有效期长access短、refresh可吊销服务端成本低需要维护刷新凭证存储典型场景内部工具、低频系统中后台产品、面向用户的系统我自己在绝大多数中后台项目里会选择双token唯一的例外是那种本身是只读报表类的展示系统用户停留时间短、交互频次低单token完全够用。3. 核心实操拦截器统一处理401刷新方案定了之后真正的重头戏在前端拦截器。这里我用axios举例因为目前后台项目里axios仍然是最普及的方案如果你用的是fetch思路完全一样只是没有现成的拦截器钩子需要自己封装一层request函数。3.1 为什么必须用拦截器而不是在每个接口里处理你可能会想“我在请求工具类里封装一个handleAuthError不就行了”实际上问题在于判断401的时机。如果每个业务页面各自调接口、各自判断错误码代码会散落得到处都是而且每个人写出来的风格还不一样。有人只判断HTTP状态401有人判断业务码是TOKEN_EXPIRED漏一处就是线上事故。正确的做法是让所有业务接口共用同一套axios实例在响应拦截器里统一拦截401业务代码完全感知不到token失效这件事。这样做的最大好处是“漏网之鱼”只可能在拦截器里排查起来只有一个文件。3.2 代码实现axios拦截器请求队列去重下面这段代码是我在真实项目中精简出来的可用版本关键逻辑都保留了// request.js import axios from axios import { getToken, setToken, clearToken, getRefreshToken } from ./auth const service axios.create({ baseURL: /api, timeout: 10000 }) let isRefreshing false let pendingQueue [] // 当前请求在刷新期间先挂起等新token出来了再放行 function subscribePending(config) { return new Promise((resolve, reject) { pendingQueue.push({ config, resolve, reject }) }) } // 新token拿到后把挂起队列里的请求全部重放 function replayPending(newToken) { pendingQueue.forEach(({ config, resolve, reject }) { config.headers.Authorization Bearer ${newToken} service(config).then(resolve).catch(reject) }) pendingQueue [] } // 请求拦截器统一塞token service.interceptors.request.use(config { const token getToken() if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器只认401 service.interceptors.response.use( response response, async error { const { response, config } error if (!response) return Promise.reject(error) const status response.status const bizCode response.data?.code // 这里用业务码避免和服务端本身的401状态码混淆 if (status 401 bizCode TOKEN_EXPIRED !config._retry) { // 已经有刷新请求在跑了直接排队等待 if (isRefreshing) { return subscribePending(config) } config._retry true isRefreshing true try { const refreshToken getRefreshToken() if (!refreshToken) { throw new Error(refresh_token missing) } // 调用刷新接口注意这里要用单独的请求不走service const { data } await axios.post(/auth/refresh, { refresh_token: refreshToken }) setToken(data.access_token) // 先重放排队中的请求再重放当前请求 replayPending(data.access_token) config.headers.Authorization Bearer ${data.access_token} return service(config) } catch (refreshError) { // refresh_token也失效清空登录态回登录页 clearToken() // 记录当前页面路由登录成功后再跳回来 localStorage.setItem(redirect_after_login, window.location.href) window.location.href /login?reasonexpired return Promise.reject(refreshError) } finally { isRefreshing false } } return Promise.reject(error) } ) export default service这里有两个细节值得展开。第一个细节刷新接口不能用service实例调用。因为如果你用了同一个实例刷新接口返回401时会被响应拦截器再拦截一次从而再次触发刷新逻辑造成无限递归。实际项目中我见过把刷新请求写成service.post(/auth/refresh)然后页面直接卡死的案例排查了半天才发现是这个原因。第二个细节为什么要引入isRefreshing布尔值和pendingQueue队列。假设用户在一个页面上同时发出了5个请求它们都带着过期token服务端全部返回401。如果没有isRefreshing5个请求都会各自去调刷新接口后端刷新接口瞬间收到5个相同刷新请求不仅浪费流量还可能因为刷新凭证轮换机制导致其中一个成功后其他4个全部失效。有了isRefreshing标志第一个401触发刷新剩下4个全部进入等待队列等新token回来后统一重放刷新接口只被调用一次。3.3 刷新后如何安全重放原始请求重放这一步是很多新手容易写错的地方。几个关键点必须给原始请求打一个_retry标记。如果不打标重放后如果新token仍然被服务端判定401比如refresh_token本身已经失效但被某个中间层放行了就会再次进入刷新逻辑形成死循环。所以每次刷新重放前都要判断!config._retry。重放请求要重新走service(config)而不是裸调用axios(config)。因为裸调用绕开了你设置的统一拦截器如果接口后续又出现401就得不到统一处理。只有继续走service整个链路才是闭环的。请求头里的Authorization要覆盖而不是追加。如果你用config.headers.Authorization ...那是覆盖没问题如果你用config.headers.common[Authorization] ...或者直接push就可能导致最终发出两个Authorization头后端解析大概率报错。我在实际操作中还踩过一个更隐蔽的坑请求拦截器里已经用某个变量缓存了旧token重放请求经过请求拦截器时又把旧token覆盖上去了。所以建议——请求拦截器里不要存死值用getToken()方法实时读取当前token。4. 多标签页与“刷新页面丢登录态”的同步问题拦截器解决的是“token过期时自动续期”但“状态登录刷新问题”还有另一个非常普遍的表现——用户刷新浏览器页面后登录状态“看起来丢了”。4.1 刷新页面时用户信息丢失如何恢复原因其实很简单Vue/Pinia或React/Redux这类状态管理库默认数据都存在内存里。按下浏览器刷新键的瞬间内存被清空store.userInfo变为空但localStorage里的token还在。结果就是路由守卫判断“有token放行”但页面一进来拿不到用户信息头像、昵称、权限菜单全空白。恢复思路并不复杂token持久化到localStorageuserInfo也要做分层缓存。我习惯的做法是登录成功后把token、refresh_token、用户基础信息id、昵称、头像等写入localStorage。在应用初始化时路由守卫里先判断本地token是否存在。如果存在调用fetchUserInfo()接口拉取最新用户信息而不是直接信任本地缓存。拉取成功后写入store拉取失败则清理登录态并跳转登录页。这里有个权衡用户基础信息里像id、昵称这类非敏感信息可以用本地缓存先顶一下页面秒开但权限点、角色这类会随时变化的敏感信息必须通过接口实时拉取否则权限变更后旧缓存会被读到刷新前。4.2 多标签页强制下线与登录状态同步多标签页是“状态登录刷新”问题里最折磨人的场景。用户开了两个标签页在标签页A点了退出登录标签页B里的状态还是登录态页面数据依然正常展示。直到标签页B某个接口返回401才开始处理而且很多实现里此时已经是“静默退出”用户根本不知道发生了什么。解决多标签页同步我推荐“本地存储监听”方案。利用storage事件当一个标签页修改了localStorage时其他标签页会收到通知// 多标签页监听登录态被清理时本页同步退出 window.addEventListener(storage, (event) { if (event.key login_state) { const state JSON.parse(event.newValue || null) if (state !state.isLoggedIn) { clearAuth() window.location.href /login?reasonkicked } } })写这段代码时有三个细节storage事件只在“其他标签页”触发本页不会触发。所以本页退出登录时你还得在自己的退出逻辑里主动清一次login_state并跳转登录页。不能用sessionStorage代替localStorage。因为sessionStorage是每个标签页独立的A页修改B页根本收不到事件。只有localStorage是跨标签页共享的。监听的范围要精确别监听整个localStorage所有key的变化。否则甚至可能有别的代码设置一个无关key你的监听器就跑一遍退出逻辑误杀情况很尴尬。如果项目对实时性要求更高还可以考虑BroadcastChannelAPI。它专门用于同源页面之间的消息通信比storage事件的体验更像“前端消息总线”能传更丰富的数据比如“token已在标签页A刷新这是新token请B页同步更新”。不过记得在页面卸载时channel.close()否则会内存泄漏。4.3 登录状态展示与全局状态恢复还有一个钝刀子割肉的问题页面没有刷新但token已经过期了用户停留在当前页面什么也不做系统不跳转也不能自动续期。等用户点击一下401才回来。很多实现此时直接把用户甩回登录页——这种体验很突兀。我更推荐的做法是做一个“会话过期遮罩层”。拦截器在捕获到401但refresh_token没有完全失效时可以先尝试静默刷新如果刷新失败不要立刻跳转而是弹一个全屏蒙层提示“登录状态已过期请点击按钮刷新”用户点击后重新走一遍登录流程登录成功后再回到之前停留的页面。这个方法在那些“用户可能停在一个页面很久”的编辑器、工单填写、审批流应用里特别实用。我做过一个巡检工单应用巡检员常常一个页面打开放在户外大半天期间网络短暂断开回来时token早已失效。如果直接踢到登录页之前填的巡检记录可能没提交就丢失了用遮罩层方案至少能提醒用户当前会话已失效且不强制清空界面现场。5. 常见问题与排查技巧速查这部分是实战踩坑的集合我把这几年里出现频次最高的问题列成一张排查表方向性排查时可以直接对着看。现象可能原因排查思路刷新token死循环接口不断被打刷新接口自身也走了拦截器触发递归刷新检查刷新请求是否独立于service实例另外刷新接口返回码不要复用业务401建议用独立码多个并发请求触发大量refresh请求没有做并发去重每个401各自刷新确认isRefreshing标志是否生效观察Network面板是否存在同时间多个refresh请求刷新成功但重放请求仍带着旧token队列里的config头部没有在重放前更新或请求拦截器用旧值覆盖在replayPending里必须显式给每个config.headers赋值请求拦截器一律通过getToken()实时取值页面刷新后store里的用户信息丢失store是内存数据刷新即清空做localStorage持久化应用初始化时重新拉取用户信息多标签页退出不同步没有跨标签页通信机制用storage事件或BroadcastChannel监听登录态变化时间不同步导致token“提前过期”客户端本地时间比服务器时间快登录校验不要依赖本地时间后续请求凭服务端返回值判断前端不自行计算过期时间refresh_token接口返回401但页面没反应刷新失败分支没被处理确认刷新catch分支里是否有清理登录态和跳转逻辑且跳转前保留回跳路径我实际排查问题时有个固定动作打开Network面板按下刷新按钮盯着第一个401请求看它的响应内容。几乎所有问题都能从这里找到线索。比如如果同一个刷新接口在几秒内被调用了多遍那就是并发去重失效如果刷新接口返回200但AccessToken没更新那就该找后端问题如果308重定向导致POST变成GET那是后端接口路径少了个斜杠和前端无关。还有一个小技巧是给拦截器加临时的日志开关。不要直接删代码用环境变量控制打印if (isRefreshing) { if (process.env.NODE_ENV development) { console.warn([auth] token刷新中请求排队等待, config.url) } return subscribePending(config) }上线前检查一遍日志级别线下调试时信息一目了然这个方法我用了很多年。6. 踩了几次坑之后的个人体会分享几个我从真实事故里总结出的原则写在这里算是给自己留个备忘录。第一token失效只认一个统一信号。我经历过一个项目后端有的接口返回HTTP 401有的返回200但业务码是TOKEN_EXPIRED还有的返回AUTH_FAILED。前端拦截器写得像拼图一样到处打补丁最后还是漏了一个接口没覆盖。后来我强制和服务端约定只要token失效一律HTTP 401统一业务码前端只认这一种信号其他一律当普通业务错误处理。第二刷新动作必须全局唯一。不管是双token还是对接统一身份中心的隐式续期同一时间只能有一个“会话续期”动作在跑。用isRefreshing布尔值可以更稳的写法是把refreshTokenWithLock设计成一个共享Promise让所有并发请求都在同一个Promise上等待let refreshPromise null function refreshTokenWithLock() { if (!refreshPromise) { refreshPromise doRefreshToken().finally(() { refreshPromise null }) } return refreshPromise }这样做的好处是即使中间某个请求等不及抛了异常其他请求依然能通过这个共享Promise等到新的token。第三重放请求必须能识别自己是“重放”。我见过最严重的线上事故是重放请求再次进入401分支后又触发一次刷新然后刷新token被后端吊销导致连环注销。所有请求重放前必须打一个防重入标记拦截器判断到这个标记就不要再进刷新分支。第四本地缓存用户信息要克制。能缓存id、昵称这类基础字段权限点、角色、员工编号这类动态数据必须实时拉取。我早期图省事把登录后返回的完整用户对象全量存进localStorage结果权限调整后用户要手动清缓存才能看到新菜单。后来改成每次“页面刷新”或“应用启动”时重新拉取用户信息问题才彻底解决。最后分享一个我一直保留的小设计在刷新失败跳转登录页前把当前页面完整路由存到localStorage登录成功后再从登录页跳回来。这个细节成本极低但对用户的体感提升特别明显。之前做审批流系统评审员填了一堆审批意见token过期弹回登录页重新登录后还能回到原来的审批页面继续提交而不是从首页重新找入口。很多人可能觉得这只是一个小体验点但我在实际项目里发现“能回到原来的页面”这个需求的反馈量一直很高。状态登录刷新问题本身就是一堆细节的组合把每一个细节都处理好用户才真的感受不到登录态的存在。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

智能车竞赛“飞跃雷区”为何需要5人组队?分工与技术栈全解析 2026/9/28 18:16:05

智能车竞赛“飞跃雷区”为何需要5人组队?分工与技术栈全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
CUB-200-2011细粒度分类实战:从数据预处理到注意力机制调优 2026/9/28 18:16:05

CUB-200-2011细粒度分类实战:从数据预处理到注意力机制调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
国内外主流大模型技术架构与特色优势深度解析:从MoE到多模态的TaoToken配置实战 2026/9/28 18:16:05

国内外主流大模型技术架构与特色优势深度解析:从MoE到多模态的TaoToken配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenClaw 最严厉的父亲:TaoToken 统一 Key 下的 Ollama num_ctx 与 Context Compaction 优化建议 2026/9/28 18:16:04

OpenClaw 最严厉的父亲:TaoToken 统一 Key 下的 Ollama num_ctx 与 Context Compaction 优化建议

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
STM32H743+LAN8720A以太网实战:D-Cache缓存一致性与性能调优 2026/9/28 18:15:58

STM32H743+LAN8720A以太网实战:D-Cache缓存一致性与性能调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
PLC+HMI+边缘AI融合控制器实战:视觉检测项目全流程解析 2026/9/28 18:15:58

PLC+HMI+边缘AI融合控制器实战:视觉检测项目全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉