news 2026/8/18 4:11:00

Vue3登录功能全栈实战:从表单到路由守卫的完整解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3登录功能全栈实战:从表单到路由守卫的完整解决方案

1. 项目缘起:为什么一个登录功能值得单独成篇?

做前端开发的朋友,尤其是刚接触Vue生态的,可能觉得登录功能不就是调个接口、存个token、跳个页面的事儿吗?我最初也是这么想的,直到在一个真实的中后台项目中,因为登录模块的“小问题”导致整个项目延期上线,我才彻底改变了看法。登录,是用户进入你应用的第一道门,也是整个前端应用安全、状态和数据流的基石。它远不止一个表单那么简单,它涉及到用户状态管理、路由守卫、接口安全、错误处理、用户体验等一系列环环相扣的环节。

这个“VueDemo-11.登录功能”项目,就是我在踩过无数坑之后,沉淀下来的一套相对完整、可复用的登录解决方案。它不是简单的axios.post('/login')就完事了,而是从零开始,构建一个健壮、安全、用户体验良好的登录模块。我会带你从最基础的登录表单开始,一步步深入到Token管理、路由权限控制、用户信息持久化等核心细节,并分享那些官方文档里不会写的“血泪教训”。无论你是Vue新手,还是想优化现有登录逻辑的老手,相信都能从中找到有价值的参考。

2. 登录模块的核心架构与设计思路

在动手写代码之前,我们先要理清一个现代SPA(单页应用)登录模块到底需要哪些部分。如果把登录功能比作一个公司的前台接待系统,那么它至少需要:一个接待表单(登录页)、一个身份核验流程(接口调用)、一个临时通行证(Token)、一个记录员工信息的工牌(用户状态)、以及一套门禁规则(路由守卫)。

2.1 技术栈选型与项目初始化

本项目基于 Vue 3 + Composition API + TypeScript + Vite 构建,这是目前Vue生态中最主流、最前沿的技术组合。选择它们的原因如下:

  • Vue 3 & Composition API:提供了更灵活、逻辑更聚合的代码组织方式。对于登录这种涉及多个响应式状态(如loading,form,errorMessage)和副作用(如API调用、路由跳转)的功能,使用<script setup>和Composition API会让代码更清晰。
  • TypeScript:为登录接口的请求/响应数据、用户信息对象、Token等提供明确的类型定义,能极大减少因数据类型错误导致的bug,尤其是在团队协作中。
  • Vite:极快的启动和热更新速度,能提升开发体验。

首先,我们初始化项目并安装核心依赖:

# 使用Vite官方模板创建项目 npm create vue@latest vue-login-demo # 按照提示选择需要的特性:TypeScript, Vue Router, Pinia cd vue-login-demo npm install # 安装UI库(这里以Element Plus为例,可根据喜好选择Ant Design Vue, Naive UI等) npm install element-plus @element-plus/icons-vue # 安装HTTP客户端和状态管理库 npm install axios pinia

2.2 目录结构规划

一个清晰的目录结构是良好架构的开始。我们的src目录会这样组织:

src/ ├── api/ # 所有接口请求封装 │ ├── modules/ # 按模块划分的接口文件,如auth.ts │ └── index.ts # 统一导出和axios实例配置 ├── stores/ # Pinia状态管理 │ └── user.ts # 用户状态存储 ├── router/ # 路由配置 │ ├── index.ts │ └── guards.ts # 路由守卫逻辑 ├── views/ # 页面组件 │ ├── Login.vue # 登录页 │ └── Home.vue # 主页(登录后) ├── components/ # 可复用组件 ├── utils/ # 工具函数,如token操作 └── types/ # TypeScript类型定义

这个结构将登录相关的逻辑清晰地分散到了api(数据)、stores(状态)、router(权限)和views(视图)中,符合“关注点分离”的原则。

3. 从零构建登录页面与表单交互

登录页是用户的第一印象,我们需要一个简洁、友好且健壮的表单。

3.1 基础表单组件搭建

views/Login.vue中,我们使用Element Plus快速搭建一个表单。这里的关键不在于UI多炫酷,而在于表单验证用户体验

<template> <div class="login-container"> <el-card class="login-box"> <h2>系统登录</h2> <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" @keyup.enter="handleLogin" > <el-form-item prop="username"> <el-input v-model="loginForm.username" placeholder="请输入用户名" :prefix-icon="User" size="large" /> </el-form-item> <el-form-item prop="password"> <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" :prefix-icon="Lock" size="large" show-password /> </el-form-item> <el-form-item> <el-button :loading="loading" type="primary" size="large" @click="handleLogin" class="login-btn" > 登录 </el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import { User, Lock } from '@element-plus/icons-vue' import type { FormInstance, FormRules } from 'element-plus' import { useUserStore } from '@/stores/user' import { useRouter } from 'vue-router' const router = useRouter() const userStore = useUserStore() // 表单引用与数据 const loginFormRef = ref<FormInstance>() const loginForm = reactive({ username: '', password: '' }) const loading = ref(false) // 表单验证规则 const loginRules: FormRules = { username: [ { required: true, message: '用户名不能为空', trigger: 'blur' }, { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' } ], password: [ { required: true, message: '密码不能为空', trigger: 'blur' }, { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' } ] } // 登录处理函数 const handleLogin = async () => { // 1. 表单预验证 if (!loginFormRef.value) return const valid = await loginFormRef.value.validate() if (!valid) return // 2. 开始加载 loading.value = true try { // 3. 调用Pinia Action进行登录 await userStore.login(loginForm) // 4. 登录成功,跳转到首页或重定向页 router.push('/') } catch (error) { // 错误处理已在userStore中统一进行,这里可以补充一些UI提示 console.error('登录失败:', error) } finally { // 5. 无论成功失败,都关闭加载状态 loading.value = false } } </script>

注意:这里将核心的登录逻辑userStore.login()抽离到了Pinia Store中,这是非常关键的一步。它保证了业务逻辑与UI组件的分离,使得登录逻辑可以被其他组件(如导航栏的退出登录)复用,也更容易进行单元测试。

3.2 表单验证的实战心得

表单验证看似简单,但细节决定体验:

  1. 即时验证 vs 提交时验证:我们采用了blur(失去焦点)触发验证,这是一种平衡。纯change(输入变化)验证会过于频繁打扰用户,而只在submit时验证反馈又太慢。blur是一个不错的折中点。
  2. 防重复提交:通过loading状态和按钮的:loading属性,有效防止了用户在请求未返回时连续点击导致的重复提交。
  3. 键盘回车支持@keyup.enter="handleLogin"是一个提升用户体验的细节,符合用户习惯。
  4. 错误信息展示:Element Plus的FormItem会自动在对应表单项下方显示message,我们无需手动控制,保持简洁。

4. 状态管理(Pinia)与Token持久化方案

这是登录功能最核心的部分之一。我们需要一个中心化的地方来管理用户的登录状态、Token和基本信息。

4.1 创建用户状态存储

stores/user.ts中,我们定义User Store:

// types/user.ts - 先定义类型 export interface ILoginForm { username: string password: string } export interface IUserInfo { id: number name: string avatar: string roles: string[] // 用于权限控制 } // stores/user.ts import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { loginApi, getUserInfoApi, logoutApi } from '@/api/modules/auth' import type { ILoginForm, IUserInfo } from '@/types/user' import { setToken, getToken, removeToken } from '@/utils/auth' export const useUserStore = defineStore('user', () => { // State const token = ref<string>(getToken() || '') // 从本地存储初始化 const userInfo = ref<IUserInfo | null>(null) const roles = ref<string[]>([]) // Getters const isLoggedIn = computed(() => !!token.value) const userName = computed(() => userInfo.value?.name || '') // Actions const login = async (loginForm: ILoginForm) => { try { // 调用登录接口 const { data } = await loginApi(loginForm) // 假设接口返回 { code: 200, data: { token: 'xxx' } } const accessToken = data.token // 1. 存入state token.value = accessToken // 2. 持久化到本地存储(关键!) setToken(accessToken) // 3. 获取用户详情信息 await getUserInfo() } catch (error) { // 清除可能已存储的token token.value = '' removeToken() // 向上抛出错误,由UI层处理(如显示错误提示) throw error } } const getUserInfo = async () => { try { // 携带token调用获取用户信息接口 const { data } = await getUserInfoApi() userInfo.value = data roles.value = data.roles || [] } catch (error) { // 获取用户信息失败,可能是token失效,执行登出 logout() throw error } } const logout = async () => { try { // 可选:调用服务端登出接口,让服务端使token失效 await logoutApi() } finally { // 无论服务端登出成功与否,前端都必须清理 resetToken() } } const resetToken = () => { // 清空state token.value = '' userInfo.value = null roles.value = [] // 清空本地存储 removeToken() } return { token, userInfo, roles, isLoggedIn, userName, login, getUserInfo, logout, resetToken } })

4.2 Token持久化的安全考量与工具函数

Token的存储位置是一个安全与便利的权衡。常见的方案有:

  1. LocalStorage:最方便,但易受XSS攻击。
  2. SessionStorage:页面关闭即消失,体验不好。
  3. Cookie (HttpOnly):可防XSS,但需注意CSRF防护,且前端JS无法直接读写。

对于大多数中后台管理系统,采用LocalStorage是主流做法,因为其便利性远大于风险(后台系统通常XSS攻击面较小)。我们可以在utils/auth.ts中封装相关操作:

// utils/auth.ts const TOKEN_KEY = 'access_token' export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY) } export function setToken(token: string): void { localStorage.setItem(TOKEN_KEY, token) } export function removeToken(): void { localStorage.removeItem(TOKEN_KEY) } // 可选:设置token过期时间(如果后端返回了expires_in) export function setTokenWithExpiry(token: string, expiresIn: number): void { const now = new Date() const expiryTime = now.getTime() + expiresIn * 1000 localStorage.setItem(TOKEN_KEY, token) localStorage.setItem(`${TOKEN_KEY}_expiry`, expiryTime.toString()) } export function getTokenWithExpiry(): string | null { const token = localStorage.getItem(TOKEN_KEY) const expiryTime = localStorage.getItem(`${TOKEN_KEY}_expiry`) if (!token || !expiryTime) { return null } if (Date.now() > parseInt(expiryTime)) { // Token已过期,清理 removeToken() localStorage.removeItem(`${TOKEN_KEY}_expiry`) return null } return token }

重要经验:永远不要只依赖前端来判断Token是否有效。前端存储的过期时间只是一个优化手段,用于提前清除明显过期的token。真正的有效性验证,必须在每次请求用户信息等敏感接口时,由后端返回的HTTP状态码(如401)来决定。前端在收到401后,应强制跳转回登录页。

5. 路由守卫与权限控制实战

路由守卫是守护应用内页面访问权限的“门卫”。没有它,用户即使不登录,也能直接通过URL访问内部页面。

5.1 路由配置与元信息

首先,在router/index.ts中定义路由,并使用meta字段标记哪些路由需要认证。

import { createRouter, createWebHistory } from 'vue-router' import type { RouteRecordRaw } from 'vue-router' const routes: Array<RouteRecordRaw> = [ { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { requiresAuth: false } // 明确标记登录页不需要认证 }, { path: '/', name: 'Home', component: () => import('@/views/Home.vue'), meta: { requiresAuth: true } // 需要认证才能访问 }, { path: '/admin', name: 'Admin', component: () => import('@/views/Admin.vue'), meta: { requiresAuth: true, roles: ['admin'] } // 需要认证且角色为admin }, // 404页面 { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue') } ] const router = createRouter({ history: createWebHistory(), routes }) export default router

5.2 实现全局前置守卫

router/guards.ts中(或在router/index.ts中直接编写),我们实现核心的守卫逻辑:

// router/guards.ts import type { Router } from 'vue-router' import { useUserStore } from '@/stores/user' import { getToken } from '@/utils/auth' export function setupRouterGuard(router: Router) { // 全局前置守卫 router.beforeEach(async (to, from, next) => { const userStore = useUserStore() const hasToken = getToken() // 从本地存储检查token // 1. 判断目标路由是否需要认证 if (to.meta.requiresAuth === false) { // 前往无需认证的页面(如登录页),直接放行 next() return } // 2. 有Token的情况 if (hasToken) { if (to.path === '/login') { // 已登录却想去登录页,重定向到首页 next({ path: '/' }) } else { // 检查用户信息是否已获取 if (userStore.userInfo) { // 已获取用户信息,检查角色权限 checkUserRole(to, userStore.roles, next) } else { try { // 未获取用户信息,则发起请求获取 await userStore.getUserInfo() // 获取成功后,再次检查角色权限 checkUserRole(to, userStore.roles, next) } catch (error) { // 获取用户信息失败(如token失效),清除token并跳转到登录页 await userStore.resetToken() next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) } } } } else { // 3. 没有Token的情况 if (to.meta.requiresAuth) { // 想去需要认证的页面,重定向到登录页,并记录目标地址以便登录后回跳 next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) } else { // 想去无需认证的页面,放行 next() } } }) } // 辅助函数:检查用户角色是否匹配路由要求的角色 function checkUserRole(to: any, userRoles: string[], next: Function) { const requiredRoles = to.meta.roles as string[] | undefined if (requiredRoles && requiredRoles.length > 0) { // 路由要求特定角色 const hasRole = userRoles.some(role => requiredRoles.includes(role)) if (hasRole) { next() } else { // 角色不符,跳转到无权限页面或首页 next({ path: '/403' }) // 需要先定义403路由 } } else { // 路由不要求特定角色,有权限即可访问 next() } }

最后,在main.ts中应用这个守卫:

// main.ts import { createApp } from 'vue' import App from './App.vue' import router from './router' import { setupRouterGuard } from './router/guards' const app = createApp(App) app.use(router) // 安装路由守卫 setupRouterGuard(router) app.mount('#app')

5.3 路由守卫的常见“坑”与解决方案

  1. 无限重定向循环:这是最常见的坑。场景:Token过期 -> 守卫跳转/login->/login页的created钩子又去检查Token -> 发现过期Token无效 -> 又触发某个跳转... 解决方案:在登录页组件中,避免在初始化时进行任何可能触发路由跳转的认证检查。或者,确保守卫逻辑中的条件判断互斥且周全。
  2. 异步获取用户信息导致的白屏:在beforeEachawait userStore.getUserInfo()时,页面会等待请求完成才渲染。如果网络慢,用户会看到白屏。优化方案:
    • 方案A(推荐):在应用根组件(如App.vue)的onMounted中主动获取一次用户信息,并存储起来。这样守卫中只需要判断userInfo是否存在,无需等待请求。
    • 方案B:对于非敏感路由(如首页),可以先放行,让页面骨架屏先出来,然后在页面组件内再去获取和渲染用户相关数据。
  3. 动态路由的权限集成:如果路由表是根据用户角色动态生成的,情况会更复杂。通常需要在登录成功后,根据角色权限,通过router.addRoute()动态添加路由,然后再进行跳转。这要求守卫逻辑能处理“路由尚未添加”的状态。

6. 网络请求层(Axios)的精细化封装

一个健壮的登录模块离不开强大的网络请求层。我们需要统一处理Token的携带、请求拦截、响应拦截和错误处理。

6.1 创建Axios实例与请求拦截器

api/index.ts中:

// api/index.ts import axios from 'axios' import type { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios' import { getToken } from '@/utils/auth' import { useUserStore } from '@/stores/user' import router from '@/router' // 创建axios实例 const service: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, // 从环境变量读取 timeout: 10000 // 10秒超时 }) // 请求拦截器 service.interceptors.request.use( (config: InternalAxiosRequestConfig) => { // 在发送请求前做些什么 const token = getToken() if (token) { // 如果token存在,将其添加到请求头 config.headers.Authorization = `Bearer ${token}` } return config }, (error) => { // 对请求错误做些什么 return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( (response: AxiosResponse) => { // 对响应数据做点什么 const res = response.data // 假设后端统一返回格式为 { code: number, data: any, message: string } if (res.code === 200) { return res.data // 直接返回业务数据 } else { // 业务逻辑错误(如密码错误) // 可以在这里统一进行消息提示 console.error(`请求错误 [${response.config.url}]:`, res.message) return Promise.reject(new Error(res.message || 'Error')) } }, (error) => { // 对响应错误做点什么 (HTTP状态码非2xx) console.error('请求失败:', error.response?.status, error.message) if (error.response) { switch (error.response.status) { case 401: // Token过期或无效 const userStore = useUserStore() userStore.resetToken() // 跳转到登录页,并携带当前页面路径 router.push(`/login?redirect=${encodeURIComponent(router.currentRoute.value.fullPath)}`) break case 403: // 权限不足 router.push('/403') break case 404: // 资源不存在 router.push('/404') break case 500: // 服务器内部错误 console.error('服务器内部错误,请联系管理员') break default: console.error(`未知错误: ${error.response.status}`) } } else if (error.request) { // 请求已发出但没有收到响应(网络错误) console.error('网络错误,请检查您的网络连接') } else { // 请求配置出错 console.error('请求配置错误:', error.message) } return Promise.reject(error) } ) export default service

6.2 按模块封装API请求

api/modules/auth.ts中,我们封装登录相关的具体请求:

// api/modules/auth.ts import request from '@/api/index' import type { ILoginForm, IUserInfo } from '@/types/user' // 登录接口 export function loginApi(data: ILoginForm) { // 注意:这里返回的是Promise,响应拦截器会帮我们提取出 res.data return request.post<{ token: string }>('/auth/login', data) } // 获取用户信息接口 export function getUserInfoApi() { return request.get<IUserInfo>('/auth/userinfo') } // 登出接口 export function logoutApi() { return request.post('/auth/logout') }

6.3 拦截器中的关键细节

  1. Token的动态获取:在请求拦截器中,我们使用getToken()工具函数从localStorage读取,而不是直接从Pinia Store读取。这是因为Store可能在页面刷新后重置,而localStorage是持久化的,能保证在应用初始化早期(Store可能还未恢复水合)拦截器也能拿到Token。
  2. 错误处理的粒度:在响应拦截器中,我们将错误分为业务逻辑错误(后端返回的特定code,如1001表示密码错误)和HTTP错误(401, 403等)。业务错误通常由调用方在catch中处理并展示给用户(如“用户名或密码错误”),而HTTP错误(如401)则在拦截器中统一处理,强制进行登出和跳转。
  3. 避免在拦截器中直接使用UI组件:拦截器是纯JS环境,无法直接使用ElMessage等UI组件进行提示。一种做法是通过触发一个全局事件(如EventBus),或者在拦截器中reject错误,由具体的页面调用处来捕获并展示UI提示。上面的示例采用了后者,将错误信息reject出去。

7. 登录功能的扩展与进阶优化

一个基础的登录流程跑通后,我们还可以从用户体验和安全角度进行诸多优化。

7.1 自动登录与登录状态保持

用户关闭浏览器再打开,或者刷新页面后,如何保持登录状态?这需要一套完整的初始化逻辑。

方案:在应用入口处初始化用户状态

App.vue或一个专门的初始化函数中:

<!-- App.vue --> <script setup lang="ts"> import { onMounted } from 'vue' import { useUserStore } from '@/stores/user' const userStore = useUserStore() onMounted(async () => { // 检查本地是否有token const token = localStorage.getItem('access_token') if (token) { try { // 有token,则尝试获取用户信息以验证token有效性 await userStore.getUserInfo() console.log('自动登录成功') } catch (error) { // token无效,清除状态 console.log('token已失效,请重新登录') userStore.resetToken() } } }) </script>

7.2 添加验证码与防暴力破解

对于安全性要求更高的系统,登录时需要图形验证码或短信验证码。

前端实现要点:

  1. 获取验证码:在登录页加载时或点击验证码图片时,请求/auth/captcha接口,后端返回一个图片Base64或唯一ID。
  2. 携带验证码:将用户输入的验证码(或验证码ID)作为参数,与用户名密码一同提交到登录接口。
  3. 错误处理:当验证码错误时,后端应返回明确错误码,前端需刷新验证码图片。
// 在Login.vue的data或store中增加 const captcha = reactive({ image: '', // base64图片 key: '', // 后端返回的验证码唯一标识 code: '' // 用户输入的验证码 }) // 获取验证码的函数 const getCaptcha = async () => { const { data } = await getCaptchaApi() captcha.image = `data:image/png;base64,${data.image}` captcha.key = data.key }

7.3 多标签页同步登录状态

当用户在A标签页退出登录,B标签页应如何感知并同步状态?可以使用localStoragestorage事件或BroadcastChannelAPI。

使用storage事件的简单方案:

App.vue或一个全局监听器中:

// 监听localStorage变化 window.addEventListener('storage', (event) => { if (event.key === 'access_token' && event.newValue === null) { // 检测到token被清空(其他标签页退出登录) const userStore = useUserStore() userStore.resetToken() // 跳转到登录页 router.push('/login') } })

在退出登录的logout函数中,主动清除localStorage,从而触发其他标签页的监听事件。

7.4 请求重试与Token刷新(双Token方案)

对于使用Access Token + Refresh Token的无感刷新方案,前端逻辑会复杂一些。

基本流程:

  1. 登录成功后,后端返回access_token(短期,如2小时)和refresh_token(长期,如7天)。
  2. 前端将两者都安全存储。
  3. access_token过期,请求接口返回401时,前端不是直接跳登录页,而是尝试用refresh_token去调用刷新接口,获取新的access_token
  4. 刷新成功后,用新token重试原先失败的请求,用户无感知。
  5. 如果refresh_token也过期或无效,则彻底跳转登录页。

这需要在Axios响应拦截器中实现一个复杂的“请求队列”和“刷新锁”机制,以避免并发请求时多次刷新Token。由于实现较为复杂,此处不展开代码,但它是构建企业级应用的重要考量。

8. 完整的登录流程串联与调试

现在,让我们把所有的碎片串联起来,看看一个完整的登录流程是如何工作的:

  1. 用户访问:用户打开应用,访问首页/
  2. 路由守卫拦截:全局前置守卫beforeEach触发。检查localStorage无Token,且目标路由meta.requiresAuthtrue。守卫将用户重定向到/login?redirect=/
  3. 登录页展示:登录页组件Login.vue加载,渲染表单。
  4. 用户提交:用户输入信息并提交,触发handleLogin
  5. 调用Store ActionhandleLogin调用userStore.login(formData)
  6. 发起网络请求:在Store的loginaction中,通过封装好的loginApi发起POST请求。Axios请求拦截器自动将Token(此时还没有)或未来的Token加入请求头。
  7. 处理响应:请求成功,后端返回Token。Store的loginaction将Token存入localStorage和Pinia state,然后调用getUserInfo获取用户详情。
  8. 状态更新与跳转:用户信息获取成功,更新Store。Login.vue中的try块执行完毕,router.push('/')跳转回首页。
  9. 再次触发守卫:跳转到/再次触发beforeEach。此时守卫检测到有Token,且已存在userInfo,检查角色权限(如有)后放行。
  10. 首页渲染:首页组件Home.vue加载,可以通过userStore获取并显示用户信息。

调试技巧与常见问题排查:

  • 问题:登录成功但不跳转,或跳转后用户信息消失。

    • 检查1:Vue Devtools中查看Pinia Store的state,确认tokenuserInfo是否已正确更新。
    • 检查2:Application面板查看localStorage,确认Token是否已持久化。
    • 检查3:Network面板查看获取用户信息的请求是否成功,响应数据是否符合预期类型。
    • 检查4:检查路由守卫逻辑,特别是hasTokenuserInfo的判断条件,以及next()的调用路径,确保没有逻辑漏洞导致无限循环。
  • 问题:页面刷新后登录状态丢失。

    • 检查:确保在App.vue或Store的初始化中,有从localStorage读取Token并尝试恢复用户状态的逻辑。
  • 问题:接口请求报401,但Token明明存在。

    • 检查1:在Axios请求拦截器中打印config.headers.Authorization,确认Token格式正确(如Bearer xxx)。
    • 检查2:检查后端Token验证逻辑和过期时间。
    • 检查3:可能是跨域问题导致请求头未携带。检查后端CORS配置,确保允许Authorization头。

构建一个完整的登录功能,就像搭建一个精密的仪表盘,每一个齿轮(表单、验证、请求、状态、路由)都必须严丝合缝。这个“VueDemo-11”项目,从最直观的登录页面开始,层层深入到状态管理、路由权限和网络封装的底层逻辑,几乎涵盖了一个生产级登录模块所需的所有核心知识。在实际开发中,你还需要根据具体的业务需求(如SSO单点登录、第三方登录、动态路由等)进行适配和扩展。希望这份详细的拆解和实战经验,能帮助你少走弯路,构建出更稳定、安全的前端应用入口。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/18 4:10:39

LLM在信息不对称博弈中的行为模式与可信度评估研究

1. 当二手车销售遇上大语言模型&#xff1a;一场信息不对称的博弈最近在琢磨一个挺有意思的事儿&#xff1a;如果让大语言模型&#xff08;LLMs&#xff09;去扮演二手车销售员&#xff0c;在信息不全的情况下跟买家讨价还价&#xff0c;它们会表现得怎么样&#xff1f;是诚实可…

作者头像 李华
网站建设 2026/8/18 4:06:15

具身多智能体系统同意链退化:从AI治理到机器人伦理的物理世界挑战

1. 项目概述&#xff1a;当AI代理的“同意链”在物理世界中断最近和几个做具身智能与多智能体系统的同行聊天&#xff0c;大家不约而同地提到了一个正在浮现的棘手问题&#xff1a;我们设计的AI代理在虚拟环境中协作得天衣无缝&#xff0c;决策链条清晰&#xff0c;授权与同意机…

作者头像 李华
网站建设 2026/8/18 4:05:43

文件包含漏洞攻防全解析:从LFI/RFI原理到实战防御

1. 项目概述&#xff1a;从“包含”到“掌控”的攻防博弈在Web安全领域&#xff0c;文件包含漏洞&#xff08;File Inclusion Vulnerability&#xff09;是一个既古老又极具杀伤力的攻击向量。它不像SQL注入那样广为人知&#xff0c;也不像XSS那样直观可见&#xff0c;但一旦被…

作者头像 李华
网站建设 2026/8/18 4:03:55

LeetCode 986题解:双指针法处理区间交集问题

1. 问题背景与核心挑战LeetCode 986题"Interval List Intersections"是一个经典的区间处理问题&#xff0c;主要考察对有序区间的操作能力。题目给定两个已排序的区间列表&#xff0c;要求返回这两个列表中所有区间的交集集合。这类问题在实际开发中非常常见&#xf…

作者头像 李华
网站建设 2026/8/18 4:02:20

从零拼出你的第一块数据大屏:DigitalTwinScreen 上手全记录

从零拼出你的第一块数据大屏&#xff1a;DigitalTwinScreen 上手全记录 【免费下载链接】DigitalTwinScreen 数字孪生可视化3d建模大屏&#xff0c;echarts,vue,cezium 项目地址: https://gitcode.com/gh_mirrors/di/DigitalTwinScreen 很多人在第一次接触"可视化大…

作者头像 李华
网站建设 2026/8/18 4:00:01

python的运筹学工业场景模拟第四十四篇:快递中转仓,多批次货物转运,中转仓容量限制,构建运输模型,求解转运分配。

快递中转仓多批次转运分配&#xff1a;用转运问题把"爆仓"变成"精准调度" "某电商物流区域分拨中心&#xff0c;每天要处理6个揽收点→3个中转仓→5个配送站的两级转运。早上6点~10点是到货高峰&#xff0c;3个中转仓的暂存面积瞬间被打满——仓1设计…

作者头像 李华