
导读后台管理系统用户登录后刷新页面就掉登录态路由守卫形同虚设接口 401 也不处理。后来用 Pinia 把登录态做成一个 storetoken 持久化到 localStorage、刷新时重新拉用户信息、路由守卫统一拦截、axios 拦截器自动带 token。一版到位这篇把完整写法贴出来。Vue3 Pinia 登录态管理token 持久化 路由守卫 用户信息刷新一版到位先说痛点。后台管理页用户登录完一切正常一刷新页面就跳回登录页token 存内存里localStorage没写路由守卫里next()逻辑写反直接死循环跳转。三个问题叠一起登录态管理等于没有。第一步userStoretoken 用户信息一个地方管Pinia 的 store 把登录态收拢// stores/user.jsimport{defineStore}frompiniaexportconstuseUserStoredefineStore(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.tokentokenthis.userInfouserInfo// 持久化token 和用户信息都要写 localStoragelocalStorage.setItem(token,token)localStorage.setItem(userInfo,JSON.stringify(userInfo))},asyncfetchUserInfo(){// 刷新页面后重新拉用户信息保证权限和头像是最新的constuserInfoawaitgetUserInfoApi()this.userInfouserInfo localStorage.setItem(userInfo,JSON.stringify(userInfo))},logout(){this.tokenthis.userInfonulllocalStorage.removeItem(token)localStorage.removeItem(userInfo)}}})刷新丢登录态的根因token 只存在内存里。state 初始化时直接localStorage.getItem(token)读回来刷新就不掉了。第二步路由守卫登录拦截 死循环防坑// router/index.jsimport{useUserStore}from/stores/userrouter.beforeEach((to,from,next){constuserStoreuseUserStore()// 白名单登录页、注册页、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){constuserStoreuseUserStore()if(userStore.token){config.headers.AuthorizationBearer${userStore.token}}returnconfig})service.interceptors.response.use((response){constresresponse.dataif(res.code401){// token 过期清登录态跳登录页constuserStoreuseUserStore()userStore.logout()router.push(/login)returnPromise.reject(newError(登录已过期))}returnres.code200?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 只清登录态 跳一次登录别重复弹窗重复跳。最终解决加个标志位防重入letisRedirectingfalseif(res.code401){userStore.logout()if(!isRedirecting){isRedirectingtruerouter.push(/login)}returnPromise.reject(newError(登录已过期))}关于持久化的选型别一上来就上 token 自动续期写 token 持久化时顺手想过要不要做自动续期token 快过期时后台静默刷新。后来忍住了——后台管理系统用户量不大静默刷新带来的复杂度刷新失败、并发刷新、续期竞态远超收益。简单方案就是 token 过期后 401 统一踢回登录页用户重新登录一次成本可接受。存储位置我也纠结过localStoragevssessionStorage。最后选了 localStorage——关浏览器重开要保持登录态这是后台系统的默认预期sessionStorage 关标签页就丢反而天天要登录。安全性上 localStorage 有 XSS 读取风险但项目里已经统一转义输出 内容安全策略可接受。真要更严的场景可以存 HttpOnly Cookie 由后端下发前端不碰 token 只碰登录状态那是另一套方案小项目不必上。可直接复用的要点token 和用户信息都持久化到 localStoragestate 初始化时读回来刷新不掉线。路由守卫登录页必须进白名单防死循环已登录访问登录页踢回首页。未登录跳登录页带上redirect参数登录成功后回跳原页面。axios 请求拦截器自动带 token响应拦截器 401 统一清登录态跳登录。401 处理加防重入标志多标签页只跳一次。刷新后onMounted重新fetchUserInfo()保证权限菜单和头像实时更新。