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的合法性、过期时间、用户权限),前端鉴权仅为辅助,防止用户跳过登录页面,不能替代后端鉴权。

相关推荐
李明卫杭州1 分钟前
CSS BFC 完全指南:从原理到实战,彻底搞懂这个"结界"
前端
裕波1 分钟前
AI 正在重写应用开发。Vue 与 Vite,给出新的答案。
javascript·vue.js
Momo__2 分钟前
MDN MCP Server——Mozilla 把 Web 文档接进 AI Agent,从此 LLM 不再瞎编 API
前端·ai编程·mcp
妙码生花2 分钟前
现代前端的极致性能 icon 加载方案(死磕成功版)
前端·vue.js·typescript
掘金者阿豪1 小时前
把业务数据变成共享仪表盘:Metabase可视化与远程访问实践
前端·后端
kyriewen1 小时前
折腾了半年 AI 编程工作流,最后发现效率瓶颈是桌上那块屏幕
前端·javascript·ai编程
蜗牛前端2 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员2 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为2 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid2 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端