Vue项目中,Token鉴权是前后端分离架构下最主流的身份验证方式,核心流程分为「Token生成→前端存储→请求携带→后端校验→Token刷新→失效处理」6个环节,全程需前后端协同配合,前端主要负责Token的存储、携带和状态管理,以下结合Vue2/Vue3实操,详细拆解每一步实现细节,所有代码可直接复制落地。
一、核心前提:Token的生成(后端主导,前端配合)
Token本身是后端生成的加密字符串(常用JWT格式,也可自定义加密规则),前端无需参与生成,仅需在用户登录成功后,接收后端返回的Token并处理。
核心逻辑:用户输入账号密码→前端发起登录请求→后端校验账号密码合法性→校验通过后,生成Token(包含用户ID、角色、过期时间等核心信息,加密处理)→后端将Token返回给前端→前端接收并存储Token,完成登录流程。
实操代码(Vue3+TS,登录接口调用)
typescript
// 1. 定义登录接口(api/login.ts)
import request from '@/utils/request'; // 封装后的axios实例
// 登录请求参数类型定义(TS可选,提升代码规范性)
interface LoginParams {
username: string;
password: string;
}
// 登录接口,返回Token和用户信息
export const login = async (params: LoginParams) => {
const res = await request.post('/api/user/login', params);
// 后端返回格式示例(需与后端协商一致)
// { code: 200, data: { token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', userInfo: { id: 1, username: 'admin', role: 'admin' } } }
return res.data;
};
二、关键步骤1:Token的前端存储(核心,决定安全性和可用性)
前端接收Token后,需选择合适的存储方式,不同存储方式各有优劣,需根据项目场景选择,常用3种存储方式,优先推荐「sessionStorage+Cookie」组合存储。
1. 3种存储方式对比及实现
| 存储方式 | 核心优势 | 潜在风险 | 实操代码 |
|---|---|---|---|
| localStorage(持久化) | 关闭浏览器后Token不丢失,无需重复登录,适配"记住密码"场景 | 易受XSS攻击,Token可能被恶意脚本窃取 | // 存储Token `` localStorage.setItem('token', res.data.token); `` // 存储用户信息(可选,用于权限控制) `` localStorage.setItem('userInfo', JSON.stringify(res.data.userInfo)); `` // 获取Token ``const token = localStorage.getItem('token'); |
| sessionStorage(会话级) | 关闭浏览器后Token自动销毁,安全性高于localStorage,避免Token残留 | 页面刷新、标签页关闭后Token丢失,用户需重新登录 | // 存储Token `` sessionStorage.setItem('token', res.data.token); `` // 获取Token ``const token = sessionStorage.getItem('token'); |
| Cookie(推荐组合使用) | 可设置httpOnly、secure,防止XSS攻击,浏览器自动携带,无需手动处理 | 跨域场景需额外配置,存储容量有限(4KB) | // 存储Token(需封装Cookie工具函数) `` setCookie('token', res.data.token, 1); // 有效期1天 `` // 获取Token ``const token = getCookie('token'); |
2. 推荐方案:sessionStorage+Cookie组合存储(兼顾安全与体验)
核心逻辑:将Token同时存储在sessionStorage(用于前端路由守卫、接口携带)和Cookie(用于后端自动校验,设置httpOnly防XSS),兼顾安全性和用户体验,实操代码如下:
typescript
// 1. 封装Cookie工具函数(utils/cookie.ts)
export const setCookie = (key: string, value: string, days: number) => {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
const expires = `expires=${date.toUTCString()}`;
// 设置httpOnly、secure(需HTTPS环境),防止XSS攻击
document.cookie = `${key}=${encodeURIComponent(value)}; ${expires}; path=/; httpOnly; secure`;
};
export const getCookie = (key: string) => {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [name, value] = cookie.split('=');
if (name === key) return decodeURIComponent(value);
}
return '';
};
// 2. 登录成功后存储Token(组合存储)
import { login } from '@/api/login';
import { setCookie } from '@/utils/cookie';
import { ElMessage } from 'element-plus';
const handleLogin = async (username: string, password: string) => {
try {
const res = await login({ username, password });
if (res.code === 200) {
// 1. 存储Token到sessionStorage(前端使用)
sessionStorage.setItem('token', res.data.token);
// 2. 存储Token到Cookie(后端校验,防XSS)
setCookie('token', res.data.token, 1);
// 3. 存储用户信息(用于角色权限控制)
sessionStorage.setItem('userInfo', JSON.stringify(res.data.userInfo));
ElMessage.success('登录成功');
// 跳转首页
router.push('/home');
}
} catch (error) {
ElMessage.error('登录失败,请检查账号密码');
}
};
三、关键步骤2:Token的携带(请求拦截器,全局统一处理)
Token存储后,需在所有请求(除登录、注册等公开接口)的请求头中携带Token,让后端校验用户身份,Vue中通过Axios请求拦截器实现全局统一携带,无需每个接口单独处理。
实操代码(Vue2/Vue3通用,Axios封装)
typescript
// utils/request.ts(封装Axios)
import axios from 'axios';
import router from '@/router';
import { ElMessage } from 'element-plus';
import { getCookie } from '@/utils/cookie';
// 创建Axios实例
const request = axios.create({
baseURL: import.meta.env.VUE_APP_BASE_API, // 环境变量配置,区分开发/生产环境
timeout: 5000,
withCredentials: true // 跨域场景下,允许携带Cookie
});
// 1. 请求拦截器:统一携带Token
request.interceptors.request.use(
(config) => {
// 排除公开接口(无需携带Token)
const publicPaths = ['/api/user/login', '/api/user/register'];
if (!publicPaths.includes(config.url as string)) {
// 从sessionStorage或Cookie中获取Token(推荐从Cookie获取,更安全)
const token = getCookie('token') || sessionStorage.getItem('token');
if (token) {
// 携带Token,格式需与后端协商(常用Bearer + Token)
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
},
(error) => Promise.reject(error)
);
// 2. 响应拦截器:处理Token相关错误(过期、无效)
request.interceptors.response.use(
(response) => response,
(error) => {
// 401状态码:Token过期、无效、未携带
if (error.response?.status === 401) {
// 清除本地存储的Token和用户信息,避免残留
sessionStorage.removeItem('token');
sessionStorage.removeItem('userInfo');
// 跳转至登录页,携带当前页面路径,登录后返回原页面(优化体验)
router.push(`/login?redirect=${router.currentRoute.value.fullPath}`);
ElMessage.error('登录已过期,请重新登录');
}
return Promise.reject(error);
}
);
export default request;
四、关键步骤3:Token的路由拦截(页面级鉴权,防止越权访问)
即使请求携带Token,仍需通过Vue Router路由守卫,拦截未登录用户访问需鉴权页面(如个人中心、管理后台),避免用户通过修改URL跳过登录,配合Token校验实现页面级鉴权。
实操代码(Vue3+Vue Router 4)
javascript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import { ElMessage } from 'element-plus';
import { getCookie } from '@/utils/cookie';
// 路由配置,通过meta.requiresAuth标记需鉴权页面
const routes = [
{
path: '/login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false } // 无需鉴权
},
{
path: '/home',
component: () => import('@/views/Home.vue'),
meta: { requiresAuth: true } // 需鉴权
},
{
path: '/profile',
component: () => import('@/views/Profile.vue'),
meta: { requiresAuth: true }
},
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, roles: ['admin'] } // 额外角色鉴权
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 全局前置守卫:拦截未登录用户
router.beforeEach((to, from, next) => {
// 无需鉴权,直接放行
if (!to.meta.requiresAuth) {
next();
return;
}
// 需鉴权,校验Token是否存在
const token = getCookie('token') || sessionStorage.getItem('token');
if (!token) {
ElMessage.warning('请先登录');
next('/login?redirect=' + to.fullPath); // 携带跳转地址
return;
}
// (可选)角色权限校验(需结合用户信息)
if (to.meta.roles) {
const userInfo = JSON.parse(sessionStorage.getItem('userInfo') || '{}');
if (!to.meta.roles.includes(userInfo.role)) {
next('/403'); // 无权限,跳转403页面
ElMessage.error('无权限访问');
return;
}
}
// Token存在且权限匹配,放行
next();
});
export default router;
五、关键步骤4:Token的刷新(无感续期,提升用户体验)
Token通常有过期时间(如1小时、24小时),过期后用户需重新登录,体验较差,因此需实现Token刷新机制(无感续期):在Token即将过期时,自动请求后端刷新Token,无需用户手动操作。
实操代码(无感刷新Token)
javascript
// utils/request.ts(在原有Axios封装基础上修改)
import axios from 'axios';
import router from '@/router';
import { ElMessage } from 'element-plus';
import { getCookie, setCookie } from '@/utils/cookie';
// 创建Axios实例(主实例,用于业务请求)
const request = axios.create({
baseURL: import.meta.env.VUE_APP_BASE_API,
timeout: 5000,
withCredentials: true
});
// 创建新的Axios实例(用于刷新Token,避免请求拦截器循环)
const refreshRequest = axios.create({
baseURL: import.meta.env.VUE_APP_BASE_API,
timeout: 5000,
withCredentials: true
});
// 标记是否正在刷新Token,避免多次发起刷新请求
let isRefreshing = false;
// 存储待执行的请求队列,Token刷新后重新执行
let requestQueue: (() => void)[] = [];
// 响应拦截器
request.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// 401状态码,且不是刷新Token的请求,且未重试过
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // 标记已重试,避免循环
if (isRefreshing) {
// 正在刷新Token,将当前请求加入队列
return new Promise((resolve) => {
requestQueue.push(() => resolve(request(originalRequest)));
});
}
isRefreshing = true;
try {
// 调用后端刷新Token接口(需携带旧Token或刷新凭证)
const res = await refreshRequest.post('/api/user/refreshToken', {
oldToken: getCookie('token') || sessionStorage.getItem('token')
});
if (res.data.code === 200) {
const newToken = res.data.data.newToken;
// 更新存储的Token
sessionStorage.setItem('token', newToken);
setCookie('token', newToken, 1);
// 更新请求头中的Token
request.defaults.headers.Authorization = `Bearer ${newToken}`;
// 执行队列中的所有请求
requestQueue.forEach(cb => cb());
requestQueue = [];
// 重试当前请求
return request(originalRequest);
} else {
// 刷新Token失败,需重新登录
throw new Error('Token刷新失败');
}
} catch (err) {
// 刷新失败,清除Token,跳转登录页
sessionStorage.removeItem('token');
sessionStorage.removeItem('userInfo');
router.push('/login');
ElMessage.error('登录已过期,请重新登录');
return Promise.reject(err);
} finally {
isRefreshing = false; // 刷新完成,重置标记
}
}
return Promise.reject(error);
}
);
export default request;
六、关键步骤5:Token的失效处理(登出+异常清理)
Token失效的场景包括:用户主动登出、Token过期未刷新、后端主动吊销Token,需统一处理:清除本地存储的Token和用户信息,跳转至登录页,避免权限残留。
实操代码(登出+异常清理)
javascript
// 1. 登出功能(components/Header.vue)
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { getCookie } from '@/utils/cookie';
import request from '@/utils/request';
const router = useRouter();
const handleLogout = async () => {
try {
// 调用后端登出接口(后端吊销Token)
await request.post('/api/user/logout', {
token: getCookie('token')
});
// 清除本地存储的Token和用户信息
sessionStorage.removeItem('token');
sessionStorage.removeItem('userInfo');
// 清除Cookie中的Token
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
ElMessage.success('登出成功');
// 跳转登录页
router.push('/login');
} catch (error) {
ElMessage.error('登出失败,请重试');
}
};
// 2. 组件卸载时清理Token(可选,防止内存泄漏)
import { onUnmounted } from 'vue';
onUnmounted(() => {
// 仅在登出、Token失效时清理,正常跳转无需清理
if (!getCookie('token') && !sessionStorage.getItem('token')) {
sessionStorage.removeItem('userInfo');
}
});
七、补充:JWT Token的特殊说明(最常用Token格式)
Vue项目中最常用的Token格式是JWT(JSON Web Token),由「头部(Header)+ 载荷(Payload)+ 签名(Signature)」三部分组成,前端可解析Payload中的用户信息(如用户ID、角色、过期时间),但不可修改(修改后签名失效,后端会拒绝校验)。
typescript
// 解析JWT Token中的用户信息(可选,用于前端快速获取用户信息)
export const parseJwt = (token: string) => {
try {
// JWT第二部分(Payload)是Base64编码,解码后可获取用户信息
const payload = token.split('.')[1];
const decoded = atob(payload);
return JSON.parse(decoded);
} catch (error) {
console.error('JWT解析失败:', error);
return null;
}
};
// 使用示例
const token = sessionStorage.getItem('token');
if (token) {
const userInfo = parseJwt(token);
console.log('用户ID:', userInfo.id);
console.log('Token过期时间:', new Date(userInfo.exp * 1000));
}
八、完整流程总结(前后端协同)
- 用户登录:前端发起登录请求,后端校验账号密码,生成Token(JWT)并返回;
- Token存储:前端将Token存储在sessionStorage+Cookie,兼顾安全与体验;
- 请求携带:通过Axios请求拦截器,给所有需鉴权请求的请求头携带Token;
- 页面拦截:通过路由守卫,拦截未携带Token的用户访问需鉴权页面;
- Token刷新:Token即将过期时,自动请求后端刷新Token,实现无感续期;
- 失效处理:用户登出、Token过期/失效时,清除本地Token,跳转登录页。
核心注意:前端Token仅负责"携带和存储",所有权限校验的核心的是后端(后端需校验Token的合法性、过期时间、用户权限),前端鉴权仅为辅助,防止用户跳过登录页面,不能替代后端鉴权。