Vue3中登录状态刷新后消失的原因在于内存数据重置。
解决方案包括:
- 使用localStorage存储token和用户信息;
- 通过Pinia持久化插件自动保存状态;
- 组合式API封装认证逻辑;
- 配合路由守卫控制访问权限;
- 使用Cookie支持SSR。
最佳实践建议采用双重存储策略(Vuex+localStorage+sessionStorage),并注意安全性和自动刷新token机制。
根据项目需求(SPA/SSR/安全级别)选择合适的持久化方案。
token 通常是 JWT(JSON Web Token)格式的字符串
Vue3登录状态持久化方案
为什么刷新后登录状态会消失?
在 Vue3 项目中,登录状态通常保存在内存中(如 Vuex/Pinia 的状态、组件的响应式数据)。当页面刷新时:
内存数据被清空- 浏览器重新加载页面,JavaScript 环境重置
Vue 应用重新初始化- store 和组件状态恢复为初始值
HTTP 请求是无状态的- 服务器不会自动记住客户端状态
如何持久化登录状态
方案一:Token + localStorage(最常用)
// 1. 登录成功后保存 token import { ref } from 'vue' const login = async () => { const response = await api.login(username, password) if (response.success) { // 保存到 localStorage localStorage.setItem('token', response.data.token) localStorage.setItem('userInfo', JSON.stringify(response.data.user)) // 同时保存到 Vue 状态管理 userStore.setUser(response.data.user) userStore.setToken(response.data.token) } } // 2. 应用初始化时恢复状态 import { onMounted } from 'vue' import { useUserStore } from '@/stores/user' const userStore = useUserStore() onMounted(() => { // 从 localStorage 恢复 const token = localStorage.getItem('token') const userInfo = localStorage.getItem('userInfo') if (token && userInfo) { userStore.setToken(token) userStore.setUser(JSON.parse(userInfo)) } })方案二:使用 Vuex/Pinia 持久化插件
安装pinia-plugin-persistedstate:
npm install pinia-plugin-persistedstate配置:
// main.js / main.ts import { createPinia } from 'pinia' import { createPersistedState } from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(createPersistedState({ storage: localStorage, // 或使用 sessionStorage // storage: sessionStorage, // 自定义序列化(可选) serializer: { serialize: JSON.stringify, deserialize: JSON.parse, }, // 自动恢复所有 store auto: true })) app.use(pinia)使用:
// stores/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: null }), actions: { login(data) { this.token = data.token this.userInfo = data.user }, logout() { this.token = '' this.userInfo = null } }, // 启用持久化 persist: { key: 'user', // 存储的 key storage: localStorage, paths: ['token', 'userInfo'] // 只持久化这些字段 } })方案三:组合式 API 封装
// composables/useAuth.js import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' export function useAuth() { const token = ref('') const user = ref(null) const router = useRouter() // 登录 const login = async (credentials) => { const response = await api.login(credentials) setAuth(response.data) } // 设置认证信息 const setAuth = (data) => { token.value = data.token user.value = data.user // 保存到 localStorage localStorage.setItem('auth_token', data.token) localStorage.setItem('auth_user', JSON.stringify(data.user)) // 设置 axios 默认请求头 api.setAuthToken(data.token) } // 登出 const logout = () => { token.value = '' user.value = null localStorage.removeItem('auth_token') localStorage.removeItem('auth_user') api.clearAuthToken() router.push('/login') } // 初始化检查登录状态 const initAuth = () => { const savedToken = localStorage.getItem('auth_token') const savedUser = localStorage.getItem('auth_user') if (savedToken && savedUser) { token.value = savedToken user.value = JSON.parse(savedUser) api.setAuthToken(savedToken) } } // 检查是否已登录 const isAuthenticated = () => { return !!token.value } return { token, user, login, logout, initAuth, isAuthenticated } }方案四:配合路由守卫
// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/dashboard', component: () => import('@/views/Dashboard.vue'), meta: { requiresAuth: true } }, { path: '/login', component: () => import('@/views/Login.vue'), meta: { guestOnly: true } } ] const router = createRouter({ history: createWebHistory(), routes }) // 路由守卫 router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') // 需要认证的页面 if (to.meta.requiresAuth && !token) { next('/login') return } // 仅游客可访问的页面(如登录页) if (to.meta.guestOnly && token) { next('/dashboard') return } next() })方案五:使用 Cookie(服务端渲染友好)
// 使用 js-cookie 库 import Cookies from 'js-cookie' // 保存 token Cookies.set('token', response.data.token, { expires: 7, // 7天后过期 secure: true, // HTTPS only sameSite: 'strict' }) // 读取 token const token = Cookies.get('token')最佳实践建议
双重存储策略:
// 同时使用多种存储方式 const saveAuth = (data) => { // 1. Vuex/Pinia store.setAuth(data) // 2. localStorage(主要持久化) localStorage.setItem('auth', JSON.stringify(data)) // 3. sessionStorage(标签页内有效) sessionStorage.setItem('auth_temp', JSON.stringify(data)) // 4. Cookie(SSR 需要) Cookies.set('token', data.token, { expires: 7 }) }安全考虑:
对于敏感信息,考虑使用
sessionStorage(标签页关闭即清除)或者使用加密存储:
npm install crypto-js
import CryptoJS from 'crypto-js' const encryptData = (data) => { return CryptoJS.AES.encrypt( JSON.stringify(data), 'your-secret-key' ).toString() }自动刷新 Token:
// 定期刷新 token setInterval(async () => { if (isAuthenticated()) { const newToken = await api.refreshToken() localStorage.setItem('token', newToken) } }, 30 * 60 * 1000) // 每30分钟
选择哪种方案取决于你的具体需求:
纯 SPA 应用:localStorage + Pinia 持久化插件
需要 SSR:Cookie + 服务端状态同步
高安全要求:短期 sessionStorage + 定期刷新 Token
简要说明:Token 是什么?
Token(令牌)是一个用于身份验证和授权的字符串凭证,类似现实生活中的“门禁卡”或“钥匙”。
核心特点
令牌形式:通常是 JWT(JSON Web Token)格式的字符串
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NSIsImlhdCI6MTYxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c无状态:服务器不需要存储 session,自身包含用户信息
自包含:Token 本身包含了用户身份信息和过期时间
Token 的工作流程(以登录为例)
用户登录 → 服务器验证 → 生成Token → 返回给客户端 ↓ 客户端存储Token → 后续请求携带Token → 服务器验证Token → 返回数据Token 的结构(JWT 为例)
一个典型的 Token 包含三部分:
头部.载荷.签名
示例解析:
// 1. 头部(Header)- 说明令牌类型和算法 { "alg": "HS256", // 签名算法 "typ": "JWT" // 令牌类型 } // 2. 载荷(Payload)- 包含实际数据(用户信息等) { "userId": "12345", "username": "张三", "exp": 1640995200 // 过期时间戳 } // 3. 签名(Signature)- 用于验证令牌真伪 HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)为什么使用 Token?
优点:
✅无状态:服务器不需要维护 session 存储
✅跨域友好:适合前后端分离和 API 调用
✅移动端兼容:适合 App、小程序等
✅可扩展性:容易实现分布式系统
缺点:
❌Token 泄露风险:一旦被盗用,攻击者可以冒充用户
❌无法立即失效:需要额外的黑名单机制
❌存储安全问题:需要安全的存储方式(localStorage 有 XSS 风险)
Token vs Cookie
| 特性 | Token | Cookie |
|---|---|---|
| 存储位置 | localStorage / 内存 | 浏览器自动管理 |
| 跨域请求 | 手动添加到 Header | 自动携带 |
| 安全性 | 易受 XSS 攻击 | 易受 CSRF 攻击 |
| 移动端支持 | 好 | 有限 |
实际应用示例
// 客户端:存储和发送 Token localStorage.setItem('token', 'your-jwt-token') // 发送请求时携带 Token fetch('/api/user', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }) // 服务器端:验证 Token app.get('/api/protected', (req, res) => { const token = req.headers.authorization?.split(' ')[1] if (!token) return res.status(401).send('无权限') // 验证 Token 是否有效 const decoded = jwt.verify(token, 'secret-key') // 如果有效,处理请求... })设置axios的通用头
App.vue
<template> <div class="container"> <Loader v-if="isLoading" text="正在加载😊" background="rgba(0,0,0,0.8)"></Loader> <global-header :user="currentUser"></global-header> <router-view></router-view> <footer class="text-center py-4 text-secondary bg-light mt-6"> <small> ...... </small> </footer> </div> </template> <script lang="ts" setup> import 'bootstrap/dist/css/bootstrap.min.css' import GlobalHeader from './components/GlobalHeader.vue' import { computed, onMounted } from 'vue' import { useStore } from 'vuex' import Loader from './components/Loader.vue' import axios from 'axios' const store = useStore() const currentUser = computed(() => { return store.state.user }) const isLoading = computed(() => { return store.state.loading }) //获取store中的token const token = computed(() => { return store.state.token }) onMounted(() => { //持久化登录状态 //判断用户是否登录 if (!currentUser.value.isLogin && token.value) { //如果没登录,但是有token //设置axios的通用头 axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}` //获取用户信息 store.dispatch('fetchCurrentUser') } }) </script>一句话总结:Token 是一种数字钥匙,客户端持有它来证明自己的身份,服务器通过验证这把钥匙的真伪来决定是否提供服务。