news 2026/10/3 6:52:54

Vue3 + Pinia 登录态管理:token 持久化 + 路由守卫 + 用户信息刷新,一版到位

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3 + Pinia 登录态管理:token 持久化 + 路由守卫 + 用户信息刷新,一版到位

导读:后台管理系统,用户登录后刷新页面就掉登录态,路由守卫形同虚设,接口 401 也不处理。后来用 Pinia 把登录态做成一个 store:token 持久化到 localStorage、刷新时重新拉用户信息、路由守卫统一拦截、axios 拦截器自动带 token。一版到位,这篇把完整写法贴出来。

Vue3 + Pinia 登录态管理:token 持久化 + 路由守卫 + 用户信息刷新,一版到位

先说痛点。后台管理页,用户登录完一切正常,一刷新页面就跳回登录页;token 存内存里,localStorage没写;路由守卫里next()逻辑写反,直接死循环跳转。三个问题叠一起,登录态管理等于没有。

第一步:userStore,token + 用户信息一个地方管

Pinia 的 store 把登录态收拢:

// stores/user.jsimport{defineStore}from'pinia'exportconstuseUserStore=defineStore('user',{state:()=>({token:localStorage.getItem('token')||'',// 初始化就从 localStorage 读userInfo:JSON.parse(localStorage.getItem('userInfo')||'null')}),getters:{isLoggedIn:(state)=>!!state.token},actions:{asynclogin(loginForm){const{token,userInfo}=awaitloginApi(loginForm)this.token=tokenthis.userInfo=userInfo// 持久化:token 和用户信息都要写 localStoragelocalStorage.setItem('token',token)localStorage.setItem('userInfo',JSON.stringify(userInfo))},asyncfetchUserInfo(){// 刷新页面后重新拉用户信息,保证权限和头像是最新的constuserInfo=awaitgetUserInfoApi()this.userInfo=userInfo localStorage.setItem('userInfo',JSON.stringify(userInfo))},logout(){this.token=''this.userInfo=nulllocalStorage.removeItem('token')localStorage.removeItem('userInfo')}}})

刷新丢登录态的根因:token 只存在内存里。state 初始化时直接localStorage.getItem('token')读回来,刷新就不掉了。

第二步:路由守卫,登录拦截 + 死循环防坑

// router/index.jsimport{useUserStore}from'@/stores/user'router.beforeEach((to,from,next)=>{constuserStore=useUserStore()// 白名单:登录页、注册页、404 不拦截constwhiteList=['/login','/register','/404']if(userStore.isLoggedIn){if(to.path==='/login'){next('/')// 已登录访问登录页,踢回首页}else{next()// 已登录,放行}}else{if(whiteList.includes(to.path)){next()// 未登录访问白名单,放行}else{next(`/login?redirect=${to.fullPath}`)// 未登录,跳登录并记住来源}}})

死循环的坑:一开始我写的判断是"未登录且不是登录页 → next(‘/login’)",结果next('/login')又触发守卫,/login不在白名单里,再跳/login,无限循环,页面直接卡死。登录页必须进白名单,这是守卫的基本盘。

第三步:axios 拦截器,自动带 token + 401 统一处理

// request.jsservice.interceptors.request.use((config)=>{constuserStore=useUserStore()if(userStore.token){config.headers.Authorization=`Bearer${userStore.token}`}returnconfig})service.interceptors.response.use((response)=>{constres=response.dataif(res.code===401){// token 过期:清登录态,跳登录页constuserStore=useUserStore()userStore.logout()router.push('/login')returnPromise.reject(newError('登录已过期'))}returnres.code===200?res.data:Promise.reject(newError(res.message))},(error)=>Promise.reject(error))

注意 Pinia 在拦截器里用必须在 store 实例化之后,useUserStore()要在函数体内调用,不能放模块顶层。

踩坑一:刷新后用户信息没了,权限菜单渲染错乱

现象:刷新页面不跳登录了,但侧边栏菜单变成"无权限",头像也空了。

排查过程:token 持久化了,用户信息没持久化——userInfo是内存对象,刷新后变 null,路由守卫只判断 token 不看用户信息,菜单组件拿不到角色权限。

定位思路:token 和用户信息要一起持久化,或者刷新后主动重新拉取。

最终解决:两个都做——state 初始化从 localStorage 读缓存,App.vue的onMounted里再fetchUserInfo()拉最新数据,保证权限实时且刷新不丢。

踩坑二:多标签页,一个页 401 全部踢下线

现象:开了三个标签页,一个接口 401,所有页面都被踢回登录页。

排查过程:401 处理里直接router.push('/login'),三个标签页的守卫同时触发,体验很糙。

定位思路:401 只清登录态 + 跳一次登录,别重复弹窗重复跳。

最终解决:加个标志位防重入:

letisRedirecting=falseif(res.code===401){userStore.logout()if(!isRedirecting){isRedirecting=truerouter.push('/login')}returnPromise.reject(newError('登录已过期'))}

关于持久化的选型,别一上来就上 token 自动续期

写 token 持久化时顺手想过要不要做"自动续期":token 快过期时后台静默刷新。后来忍住了——后台管理系统用户量不大,静默刷新带来的复杂度(刷新失败、并发刷新、续期竞态)远超收益。简单方案就是 token 过期后 401 统一踢回登录页,用户重新登录一次,成本可接受。

存储位置我也纠结过:localStoragevssessionStorage。最后选了 localStorage——关浏览器重开要保持登录态,这是后台系统的默认预期;sessionStorage 关标签页就丢,反而天天要登录。安全性上 localStorage 有 XSS 读取风险,但项目里已经统一转义输出 + 内容安全策略,可接受。真要更严的场景,可以存 HttpOnly Cookie 由后端下发,前端不碰 token 只碰登录状态,那是另一套方案,小项目不必上。

可直接复用的要点

  • token 和用户信息都持久化到 localStorage,state 初始化时读回来,刷新不掉线。
  • 路由守卫:登录页必须进白名单,防死循环;已登录访问登录页踢回首页。
  • 未登录跳登录页带上redirect参数,登录成功后回跳原页面。
  • axios 请求拦截器自动带 token,响应拦截器 401 统一清登录态跳登录。
  • 401 处理加防重入标志,多标签页只跳一次。
  • 刷新后onMounted重新fetchUserInfo(),保证权限菜单和头像实时更新。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 6:52:18

OpenClaw定时任务配置:让AI自动干活,TaoToken统一Key接入实战

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

作者头像 李华
网站建设 2026/10/3 6:51:56

基于PLC的升降横移式立体车库自动存取系统设计

三年前我带学生做了一套三层五列升降横移式立体车库的PLC控制系统,题目就是简简单单几个字:基于PLC的立体车库自动存取系统设计。很多计算机专业的学生看到“PLC”三个字就发怵,觉得这是电气专业的东西,跟自己的毕设不搭。实际上这…

作者头像 李华
网站建设 2026/10/3 6:51:39

嵌入式调试中的偶发bug排查:串口、蓝牙与烧录实战指南

搞嵌入式或者说做硬件调试的,应该都有过这种经历:一个bug挂在测试列表里好几天,你盯着它的时候它老老实实,你一松懈它就冒出来,而且往往只出现一次。串口不过数据、蓝牙握手失败、烧录校验不通过,这类“偶发…

作者头像 李华
网站建设 2026/10/3 6:51:03

M8连接器工业应用全解析:选型、接线与故障排查

M8连接器这种小东西,在自动化产线里存在感极低,但真出了毛病,整条线都能给你停半小时。我之前在一个汽车焊装车间处理过一次报警,IO盒上的M8插头接触不良,导致临近传感器信号时断时续,机械手停在中途&#…

作者头像 李华