☰
Vue刷新丢登录状态?一文详解Token原理与7天免密登录实现
2026/10/6 4:15:02 网站建设 项目流程

你有没有遇到过这种情况:好不容易把Vue项目开发完,登录、跳转、拉取用户信息、权限控制全都跑通了,测试小姐姐也在验收单上签了字。结果你在浏览器里随手按了一下F5,页面转了一圈,又回到登录页。当场血压拉满。

这种“刷新丢登录状态”的问题,在Vue项目里几乎是新人必踩的坑,而且踩法五花八门——有人是刷新后白屏,有人是刷新后接口全部401,有人是明明token还在localStorage里,路由却偏说没登录。我甚至见过一个项目把token存进了sessionStorage,然后跟产品吵了一下午“为什么用户关掉浏览器再打开就要重新登录”。

这篇文章就围绕三个问题展开:刷新为什么会丢登录状态、Token到底是什么、以及网站常见的“7天免密登录”核心原理是怎么做的。我会把排查思路、代码实现、参数怎么定、坑在哪里全部写出来,拿捏不准的可以直接抄作业。

1. 从一次“刷新丢登录”的尴尬现场说起

1.1 先给问题画个像

“刷新丢登录”这个描述其实很笼统,不同项目表现不一样,但根因往往相似。先梳理一下最典型的几个症状:

  • 刷新后跳回登录页,而且localStorage里确实没有token了
  • 刷新后路由没跳,但所有请求都返回401
  • 刷新后接口能通,但页面上的用户信息全空了,像是“半个登录状态”
  • 只在一部分浏览器或者隐身模式下复现

这些问题经常混在一起出现,又会让开发者误以为是“Vue的问题”“路由的问题”“axios的问题”,但实际上绝大多数跟框架本身没有半毛钱关系。Vue也好、React也好,刷新页面之后浏览器都会重新加载整个文档,JavaScript运行时的内存状态(也就是你存在变量里的那些数据)全部清空,应用要重新初始化一遍。这意味着:只要token没有落在持久化存储里,或者应用初始化时没有主动去恢复它,登录状态就是必丢的。

1.2 排查前必须搞清楚的链路

想解决这个问题,得先画出一条“登录状态生命周期”链路。我做过的几个项目里,凡是能三分钟定位到问题的人,脑子里的链路基本一致:

  1. 用户在登录页输入账号密码,提交给后端
  2. 后端校验通过,返回一个token(以及用户信息)
  3. 前端拿到token,存到某个地方(localStorage / sessionStorage / cookie / vuex)
  4. 之后每次请求,前端把token塞进请求头或Cookie里
  5. 刷新页面,应用重新启动,路由守卫/初始化逻辑去读存储里的token
  6. 如果读到了,放行并拉取用户信息;如果没读到,踢回登录页

“刷新丢登录状态”这件事,本质上就是第5步出了问题——要么没读到,要么读到了但不认为是有效的。排查的时候不要一上来就翻路由守卫,而是先从“刷新后存储里到底还有没有token”这一步开始。我见过有人排查了一整天,最后发现是登录时压根没做存储,token只挂在内存变量上,刷新必丢——这种低级问题只要按链路捋一遍,五分钟就能定位。

提示:把“登录状态链路”写下来打印出来贴工位旁边,比记一堆框架API有用得多。排查问题先分环节,再定位环节,效率会高非常多。

2. Token到底是个什么东西

2.1 从“会话”到“凭证”的进化史

很多同学在做登录功能时,是把“token”当成一个黑盒来用的——后端返回什么就存什么,要什么就给什么。但你想把7天免密登录做好、想排查清楚刷新丢登录的根因,就必须理解Token在设计上到底解决了什么问题。

咱们先用大白话捋一下历史。最早的Web登录靠的是Cookie + Session:用户登录后,服务器在内存或数据库里创建一个会话记录,把会话ID通过Set-Cookie塞给浏览器,浏览器以后每次请求自动带上这个Cookie,服务器比对一下会话ID就知道你是谁。这方案到今天也没过时,但有个痛点——如果后端是多实例部署(比如负载均衡挂了三台服务器),会话存在了第一台机器上,第二台机器不认识,用户请求就会被随机踢出去。后来有了粘性会话、Session共享、Redis集中存储,但代价都是引入额外的设施和复杂度。

Token的思路换了赛道:服务器不再存“会话状态”,而是直接把用户身份和权限信息签名打包成一个字符串交给客户端,客户端请求时原样带回来,服务器验签通过就认账。这样一来,服务端变成无状态的,哪台机器都能验,也就天然适合分布式部署。

2.2 JWT的结构和有效期设计

目前最主流的Token形态是JWT(JSON Web Token),它的结构是“三段式”:头部(Header)、载荷(Payload)、签名(Signature),用点号拼接。头部声明算法类型,载荷放用户ID、过期时间等业务数据,签名是用密钥对前两段内容进行的哈希加签。

// 一个典型JWT长这样 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

其中载荷里有三个时间字段需要你格外关注:iat(签发时间)、exp(过期时间)、nbf(生效时间,不常用)。为什么要关注?因为很多“刷新后突然没登录”的问题,本质就是exp算得太短,你登录完随手刷新一下,token已经过期了。后端如果设了比如5分钟有效期,那就是有意为之,需要配合刷新机制;但如果后端稀里糊涂把过期时间设成几秒钟,那前端再怎么折腾都是白搭。

JWT的优势是自包含,缺点是“无法主动作废”——只要token没过期,就算用户改了密码,旧token在服务端验签时依然是合法的。所以纯JWT方案里,服务端一般还要配合黑名单/白名单,或者引入我们后面要讲的refresh token机制来缓解这个问题。

2.3 为什么不能把Token塞进URL

这一小节属于安全扫盲,但跟登录状态息息相关。

有人为了方便调试,把token通过query string传参,比如/userInfo?token=xxxx。这在本地开发看着很爽,但风险极大:URL会出现在浏览器历史记录、服务器访问日志、nginx日志、CDN日志、各种第三方分析工具里,等于把钥匙复制了好几份贴在大门口。真正的token应该放在请求头里,比如Authorization: Bearer <token>,或者放在带HttpOnly属性的Cookie里。

这里还有个容易被忽略的细节:如果你的token放在localStorage里,那么理论上任何能执行脚本的XSS漏洞都能把它偷走。这也是为什么部分安全要求高的项目,会把token存进HttpOnlyCookie里,让JS根本读不到。但HttpOnly Cookie的缺点是没法在JS里手动操作,跨域场景也麻烦。所以在实际项目里,token放哪要看你团队对XSS防护和跨域能力的信心,没有绝对最优解,只有权衡。

3. 刷新页面后登录状态丢失的四个典型原因

3.1 存储位置选错了:sessionStorage的陷阱

我见过最典型的“刷新丢登录”,就是把token存进了sessionStorage。从名字上看,sessionStorage好像和“会话”很配,于是很多新手会下意识往里塞登录凭证。但它的生命周期是:页面会话期间有效,关闭标签页或浏览器后就没了;用新标签页打开站点时,它也是空的。刷新页面时sessionStorage不会丢,但为什么很多人觉得“存了sessionStorage刷新就丢”?其实真正的坑是浏览器崩溃恢复、隐身模式限制、以及用户习惯性关掉标签页再开——这些场景下sessionStorage就清空了,用户感知就是“我明明登录过,怎么又要输密码”。

而localStorage是持久化的,只要用户不主动清浏览器数据,它就能一直在。对于“保持登录状态”这个需求来说,首选持久化存储基本就是localStorage(或者HttpOnly Cookie,后面细讲)。

// 错误示范:存sessionStorage sessionStorage.setItem('token', res.data.token) // 正确姿势:存localStorage localStorage.setItem('token', res.data.token)

但localStorage也不是高枕无忧,它有另一个问题:多个标签页之间没有自动同步机制。一个标签页登录了,另一个标签页不知道;一个标签页登出了(清了localStorage),另一个标签页还以为是登录状态。后面会专门讲怎么处理。

3.2 初始化时序问题:路由守卫等不到token

这个坑更隐蔽。表现为:你明明在localStorage里看到了token,但刷新后还是被踢回登录页。问题往往出在初始化时序上。

vue-router的路由守卫和应用的初始化是同步执行的,但获取用户信息的请求是异步的。常见的错误写法是这样:

router.beforeEach(async (to, from, next) => { const token = localStorage.getItem('token') if (!token) { next('/login') return } // 这里拉用户信息,假设是个异步请求 const user = await api.getUserInfo() if (!user) { next('/login') } else { next() } })

你看着逻辑没问题:有token就拉用户信息,没token就去登录页。但实际运行时,如果用户信息接口返回401(因为token过期了,或者后端临时抽风),这个守卫就会把用户踢回登录页。这不算“时序”问题,算“判断逻辑不严谨”。

真正的时序问题长这样:应用启动时,路由守卫先跑了,但axios的请求拦截器还没挂载好,导致api.getUserInfo()发出的请求没带上token,后端返回401,于是守卫误判“用户未登录”。这种问题在你是用了模块化加载、异步插件或者某些依赖注入顺序不当的时候特别容易发生。

解决思路是:把“初始化用户信息”这个动作从路由守卫里抽出来,做成一个独立的、幂等的方法,确保它在所有axios拦截器就绪后再执行;路由守卫只判断“有没有token”,尽量不做异步验证(要验证也通过拦截器的全局错误处理统一做)。

3.3 Token过期:后端直接给你401

这是“刷新丢登录”里最符合逻辑的一种情况:不是没存,不是没读,而是token真的过期了。

前端要在登录状态这件事上做得让人“无感”,核心就是自动续期。很多小项目图省事,token有效期给得特别长,比如30天、甚至一年,但这是安全上的大忌——偷到一个能用一年的token,等于拿到了全年通行证。

正规做法是双token(后面第4部分重点展开):一个短期token用来访问接口,过期时间控制在15分钟到2小时;一个长期token用来换取新的短期token,有效期可以是7天、30天。这样用户在使用过程中无感知,长时间不用的会话才需要重新登录,同时还能控制被盗用的风险。

3.4 多标签页同步与localStorage被清空

localStorage虽然持久,但有一个特性经常被忽略:它是按“来源(origin)”隔离的,同一个浏览器、同一个域名下的所有标签页共享同一份localStorage,但标签页之间不会有storage变化的实时通知(实际上有storage事件,但只在别的标签页里触发)。

想象一个场景:用户开了两个标签页,A标签页登录了,B标签页还是登录页。用户在B标签页登录,A标签页没有任何感知,两个页面都认为自己是“当前登录态”。如果A页面发了一个登出接口,清了localStorage里的token,B页面的下次请求就会带着一个已经被后端作废的token,然后被401,再被自己的拦截器踢回登录页。

处理多标签页登录状态同步,主要有两个手段:

  • 监听storage事件,当其他标签页修改localStorage时,本页面重新读取、更新内存里的登录状态
  • 不用localStorage,直接用Cookie。Cookie在标签页间天然共享,不需要手动同步

但我得说句实话:多标签页的“完全同步”很难做到完美,因为很多操作(比如登出)本质上是服务端状态的变化,前端只能靠请求结果来感知。实际项目里,先把“刷新不丢登录”这个问题解决掉,多标签页同步通常够用就行:监听storage事件做强制下线或登出操作,已经是相当完善的处理了。

4. 7天免密登录的核心技术原理

4.1 双Token体系:为什么需要两个

现在我们来聊“7天免密登录”这件事。先澄清一个概念:这个词里的“免密”不是指“不用密码”,而是指“在有效期内不需要重复输入密码”。7天免密登录的技术本质,就是让前端保存一个能够“自我证明身份”的长期凭证,在有效期内用它静默换取一个新的短期凭证。

这里要用到双Token设计:

项目access_tokenrefresh_token
作用请求业务接口的身份凭证换取新的access_token
有效期短,通常15分钟~2小时长,7天~30天
存储位置内存、localStorage、Cookie均可更敏感,建议放HttpOnly Cookie或更安全的存储
前端行为每次请求带上只在access_token过期时使用
泄露风险相对较低相对较高,必须重点保护

为什么不让access_token直接给7天有效期,反而搞两个token这么麻烦?原因很简单:安全与体验的折中。短期token如果被盗,黑客能作恶的时间窗口很短;长期token虽然更危险,但我们可以把它藏得更深(比如HttpOnly Cookie,XSS拿不到),并且每次使用refresh_token都会换一个新的,一旦发现异常可以直接吊销。

4.2 免密登录的完整流程

把双Token串起来,7天免密登录的流程大概是这样:

  1. 用户输账号密码登录,后端校验成功,返回两个token:一个7天有效期的refresh_token,一个1小时有效期的access_token
  2. refresh_token写入HttpOnly Cookie,access_token放内存(或localStorage)
  3. 用户正常使用,接口都带access_token,一切无感
  4. access_token快过期了,前端在某个时机(比如请求返回401)用refresh_token去换取新的access_token
  5. 后端验证refresh_token有效且没过期,签发一个新的access_token
  6. 用户连续使用了7天,第8天refresh_token过期,请求401,前端清除登录状态,跳转登录页,用户需要重新输密码

这套流程里最核心的细节是:access_token失效后,前端不能用“踢回登录页”当默认行为,而是要先尝试静默续期。只有refresh_token也失效时才真正需要用户重新登录。

4.3 axios拦截器:无感续期的落地实现

在Vue项目里,续期动作通常放在axios的响应拦截器里做。思路非常简单:收到401就调刷新接口,刷新成功就重发原来的请求,刷新失败才跳登录页。

// 伪代码,重点看思路 let isRefreshing = false let waitingQueue = [] service.interceptors.response.use( response => response, error => { const { response, config } = error if (response.status === 401 && !config._retry) { if (isRefreshing) { // 已经有请求在刷新token了,把其他401请求挂起,等刷新完重放 return new Promise(resolve => { waitingQueue.push(() => { config._retry = true resolve(service(config)) }) }) } isRefreshing = true config._retry = true return refreshToken() .then(newToken => { updateToken(newToken) waitingQueue.forEach(cb => cb()) waitingQueue = [] return service(config) }) .catch(err => { waitingQueue = [] redirectLogin() return Promise.reject(err) }) .finally(() => { isRefreshing = false }) } return Promise.reject(error) } )

那段waitingQueue的设计,是很容易被忽略的细节:并发请求同一时间全部401时,如果每个请求都去调刷新接口,会打出好几发刷新token请求,白白浪费网络,还会因为重复使用refresh_token导致后端强制下线。正确的做法就是加一个锁,让第一个401请求去刷新token,其余的排队等新token回来再重发。

4.4 安全是极限拉扯:有效期到底怎么定

“7天免密登录”里的7天,并不是拍脑门定的数字。它取决于你的业务风险承受能力和用户预期:

  • 金融、支付类应用:免密窗口极短,甚至禁用,每次都要重新验密或二次验证
  • 工具类、内容类应用:7天~30天比较常见
  • 企业内部系统:可能直接做“记住我”勾选,不勾就关闭浏览器即失效,勾了就7天有效

另外,refresh_token最好是**一次性(rotate)**的:每次用它换新token时,旧refresh_token立刻作废。这样即便refresh_token被截获,黑客拿到时可能已经失效,或者旧token被用了两次会触发服务端警报。后端还要记录每个refresh_token的“父token”,一旦发现链被续得太长或者出现异常跳跃,就判定为盗用,强制全部下线。

注意:任何“7天免密”方案里,登出操作必须同时清除本地token和后端refresh_token的会话记录。很多人以为登出就是前端清一下localStorage,结果旧token还能用,被安全测试一抓一个准。

5. 实操:一套可落地的登录状态保持方案

5.1 先封装一个token读写模块

不管用什么存储,先把操作集中封装起来,别在代码里到处写localStorage.getItem('token')。这后面要改存储方式、要做多标签页同步,都只需要改一个文件。

// utils/token.js const ACCESS_TOKEN_KEY = 'app_access_token' const REFRESH_TOKEN_KEY = 'app_refresh_token' export function getAccessToken() { return localStorage.getItem(ACCESS_TOKEN_KEY) } export function setAccessToken(token) { localStorage.setItem(ACCESS_TOKEN_KEY, token) } export function getRefreshToken() { return localStorage.getItem(REFRESH_TOKEN_KEY) } export function setRefreshToken(token) { localStorage.setItem(REFRESH_TOKEN_KEY, token) } export function clearTokens() { localStorage.removeItem(ACCESS_TOKEN_KEY) localStorage.removeItem(REFRESH_TOKEN_KEY) }

这里只演示了localStorage的版本。如果项目里有HttpOnly Cookie存refresh_token,那么getRefreshToken/setRefreshToken就换成接口调用的方式,比如POST /auth/refresh,token本身从Cookie里自动带上,前端JS根本不需要去读它。声明这套接口,是为了让你在换存储方案时不用翻遍整个src目录。

5.2 路由守卫 + 初始化用户信息

路由守卫的职责要尽量单一:只做“有没有token”的判断,不做“token是否有效”的验证(因为中间要跑异步接口,时序问题容易缠成一团)。

import router from './router' import { getAccessToken } from '@/utils/token' import { fetchUserInfo } from '@/api/user' router.beforeEach(async (to, from, next) => { const token = getAccessToken() if (!token) { if (to.path === '/login') { next() } else { next({ path: '/login', query: { redirect: to.fullPath } }) } return } if (to.path === '/login') { next('/') return } // 用户信息还没初始化,才去拉取 if (!store.state.user.info) { try { const user = await fetchUserInfo() store.commit('setUser', user) } catch (e) { // token失效,清除本地凭证,跳登录 clearTokens() next({ path: '/login', query: { redirect: to.fullPath } }) return } } next() })

这段代码的关键点有几个:

  • 用户信息是异步拉取的,但只要拉到过一次就存进Vuex/内存,后面不再重复请求。刷新后内存清空,重新拉一次,但这次拉取发生在路由守卫里,axios拦截器此时已经挂载完毕(这个时序要保证,办法是确保引入路由守卫这个模块的副作用执行顺序在axios拦截器之后)
  • 跳转登录页时带上redirect参数,登录成功后能回到原来的页面,体验细节
  • 拉取用户信息失败,说明token大概率挂了,这时候再去走刷新token的逻辑,而不是直接把用户踢走

有个非常容易犯的错:在路由守卫里一看到401就跳登录页。401不代表登录失效,它可能是access_token过期,refresh_token还活着。真正该做的动作是“尝试刷新token,刷新失败再跳登录页”。

5.3 让axios拦截器去解决401

前面已经贴了刷新token的核心拦截器代码。这里补充几个实现要点:

  • 刷新接口本身不能被拦截器递归拦截。如果刷新接口也返回401,千万别让它再走进“刷新token”的逻辑,否则就是死循环。通常直接在拦截器里判断请求的url,或者给刷新请求加上特殊标记。
  • 等待队列要防止内存泄漏。挂起的请求如果一直等不到token刷新完,最后要清空队列,并给所有挂起请求返回错误,不能让请求永久pending。
  • 刷新token也失败时,要清空所有本地token,跳登录页,并且别弹一堆错误的toast。用户感知应该是“登录过期,请重新登录”,而不是一堆401红色报错。
// 刷新接口打上标记,避免递归 const REFRESH_URL = '/auth/refresh' service.interceptors.response.use(null, error => { const { url, config } = error if (url === REFRESH_URL) { clearTokens() redirectLogin() return Promise.reject(error) } // 其他401逻辑... })

这个“刷新接口自己不能再触发刷新”的坑,我踩过不止一次。第一次上线的时候,刷新token请求被别人误加了Authorization头,后端返回401,然后又触发了刷新逻辑——两个刷新请求互相嵌套,控制台直接刷屏。

5.4 常见问题排查速查表

把这些年遇到过的相关问题的排查步骤整理成了一张表,配合排查链路用,效率很高:

现象排查步骤解决方案
刷新后被踢回登录页,localStorage没有token检查登录成功后是否真的执行了setAccessToken在登录页的提交回调里断点,确认响应数据和存储代码都执行了
localStorage有token,但还是跳登录页检查路由守卫是否异步拉用户信息失败406 + 刷新token机制;或把用户信息拉取放到守卫中的try/catch并区分401和网络错误
接口401,但路由不跳登录页检查axios拦截器是否正确捕获了401统一在响应拦截器处理401,别在页面代码里单独catch
刷新token后,并发请求大量失败检查是否有等待队列去重用isRefreshing标志 + 请求队列,只让一个请求去刷新
多标签页一个登出全部掉线检查storage事件是否监听,以及Cookie方案是否可行监听storage事件,登出时主动通知其他标签页
浏览器关闭再打开需要重新登录token存进了sessionStorage而不是localStorage换成localStorage/Cookie
隐身模式下刷新丢登录localStorage在隐身模式下可能被限制或清空两种方案:继续用localStorage并提示用户隐私模式的限制;或改用基于Cookie更稳定但更麻烦的存储

5.5 我踩过的几个坑

单独聊几个实操里容易翻车的地方。

第一个坑:登录后在路由守卫里“立刻”拉用户信息。路由跳转比接口响应快得多,用户到了首页,但页面因为用户信息还没加载完,会渲染出一堆空数据。我们后来加了全局的initData方法,在应用初始化完成后,统一拉取基础信息,而不是只在路由守卫里干活。这样逻辑清晰,页面每个组件的加载也不依赖路由时序。

第二个坑:刷新token使用了自己的localStorage。如果refresh_token存在localStorage里,而你的站点被植入了一个XSS脚本,黑客可以直接替用户发起刷新请求,把所有信息偷走。后来我们给refresh_token改成了HttpOnly Cookie,前端彻底读不到,访问接口时浏览器自动带上,安全性上升了一个台阶。代价是跨域时Cookie要配SameSite=None; Secure,稍微麻烦一点。

第三个坑:忘了处理“登出其他标签页”。用户在一个标签页登出,另一个标签页还在正常操作,点按钮时API返回401,才一脸懵地发现自己被踢了。这种感觉很割裂。我们后来在所有页面的mounted里监听了storage事件,一旦发现token键被移除,立刻跳转登录页并提示“已在其他窗口登出”。

第四个坑:刷新token时死循环。这个前面提过,刷新接口本身也会返回401,如果不做特殊处理,无限递归会把浏览器网络请求刷爆。一定要在拦截器里判断URL,或者给刷新请求加一个_refreshFlag。

6. 写在最后:这套方案的取舍与扩展

如果项目刚起步、用户量不大,可以不用上双Token那么重的一套体系,先做一个“长效token + 路由守卫 + localStorage”的简化版也能跑。但如果你问我在实际项目里最后用的是哪套方案,我会说:access_token放内存(每次刷新页面自动续期),refresh_token放HttpOnly Cookie,7天有效期,配合token轮换,登录接口记住登录状态。这套方案兼顾了安全性、体验和可维护性,虽然写起来比无脑存localStorage多几百行代码,但换来的是产品不用跟用户反复解释“为什么我又要登录了”。

再补充一个小经验:登录状态的保存方案一定要在项目一开始就定下来,别等上线后再改。改存储方案看着只是换一个API,实则涉及路由守卫、请求拦截器、多标签页同步、后端会话策略,四处都要跟着动。我在一个项目上线第二个月时做过一次从sessionStorage改到Cookie的迁移,改了三天,还出了一堆线上bug。如果能在代码评审阶段就把这套链路走一遍,后面能省非常多的心。

7天免密登录这件事,往深了做,还能引申出设备管理、异地登录提醒、登录日志审计、多端互踢等功能。但只要把“刷新不丢登录”“401自动续期”“logout清干净”这三件事做扎实,你的Vue项目在登录这块就已经达到中上水准了。

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

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

立即咨询