Vue Token鉴权避坑指南|5步完整实现(从生成到失效全解析)

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));
}

八、完整流程总结(前后端协同)

  1. 用户登录:前端发起登录请求,后端校验账号密码,生成Token(JWT)并返回;
  2. Token存储:前端将Token存储在sessionStorage+Cookie,兼顾安全与体验;
  3. 请求携带:通过Axios请求拦截器,给所有需鉴权请求的请求头携带Token;
  4. 页面拦截:通过路由守卫,拦截未携带Token的用户访问需鉴权页面;
  5. Token刷新:Token即将过期时,自动请求后端刷新Token,实现无感续期;
  6. 失效处理:用户登出、Token过期/失效时,清除本地Token,跳转登录页。

核心注意:前端Token仅负责"携带和存储",所有权限校验的核心的是后端(后端需校验Token的合法性、过期时间、用户权限),前端鉴权仅为辅助,防止用户跳过登录页面,不能替代后端鉴权。

相关推荐
Momo__1 小时前
package.json 配置详解:依赖管理深度指南
前端
漫游的渔夫1 小时前
前端开发者做 Agent:模型说执行就执行?先加 3 道闸门再碰真实业务
前端·人工智能·typescript
前端那点事1 小时前
企业级Vue前端鉴权方案全解析|从Token到OAuth2.0,覆盖多端适配+权限管控
前端·vue.js
亲亲小宝宝鸭1 小时前
从Vben-Admin里面学习hooks
前端·vue.js
Mintopia1 小时前
MSW Mock Feature-First 方案
前端·架构
sin6031 小时前
Talk is cheap 之后:AI Agent 时代,程序员真正要交付什么?
前端
Ticnix1 小时前
手把手教你在 Next.js 中接入本地大模型,实现 ChatGPT 同款流式对话
前端·next.js
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_18:(HTML 表格进阶特性与无障碍——从标题结构到屏幕阅读器适配)
前端·笔记·ui·html·音视频
沐 修1 小时前
前端调试 - 获取下拉框元素 F12 延时断点操作记录 - 秒杀其他所谓的F8和手速快操作
前端