Vue + Axios + Node.js(Express)如何实现无感刷新Token?

作者:郭晟玮日期:2025/11/19

在前后端分离架构中,Vue 前端配合 Axios 发起请求,Node.js(Express)搭建后端服务时,可实现 Token 无感刷新以提升用户体验。具体而言,前端 Vue 项目通过 Axios 拦截器,在每次请求前检查 Token 状态。若 Token 即将过期,先向服务端发起静默刷新请求,Express 后端验证旧 Token 后颁发新 Token。前端拦截器收到新 Token 后,将其更新到本地存储,并重新发起原请求,整个过程对用户透明,无需手动重新登录。

页面基本流程

  1. 登录成功后,后端返回 Access Token 和 Refresh Token,前端存储两者及各自有效期。
  2. 每次发起业务请求前,前端判断 Access Token 是否即将过期。
  3. 若即将过期,先调用 “刷新 Token 接口”,用有效的 Refresh Token 换取新的 Access Token。
  4. 用新的 Access Token 发起原业务请求,用户全程无感知。
  5. 若 Refresh Token 也过期,才会引导用户重新登录。

一、技术栈与核心约定

  • 前端:Vue 3(适配 Vue 2,只需微调语法)+ Axios(统一请求拦截)
  • 后端:Node.js + Express + JWT(生成 Token)+ Redis(存储 Refresh Token,可选但推荐)
  • Token 规则:
    • Access Token:短期有效(1 小时),用于业务请求身份验证
    • Refresh Token:长期有效(7 天),仅用于刷新 Access Token
    • 状态码:401 = Access Token 过期 / 无效;403 = Refresh Token 过期 / 无效

二、前端实现(核心代码)

1. 初始化 Axios 实例(api/index.js)

封装请求 / 响应拦截器,处理 Token 携带、刷新和重试逻辑:、

1import axios from 'axios';
2import { ElMessage } from 'element-plus'; // 按需引入 UI 组件库提示(可选)
3
4// 1. 创建 Axios 实例
5const service = axios.create({
6  baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量配置后端地址
7  timeout: 5000, // 请求超时时间
8});
9
10// 2. Token 存取工具函数(安全存储建议用 HttpOnly Cookie,此处用 localStorage 演示)
11const TokenKey = {
12  ACCESS: 'access_token',
13  REFRESH: 'refresh_token',
14};
15
16// 获取 Token
17const getAccessToken = () => localStorage.getItem(TokenKey.ACCESS);
18const getRefreshToken = () => localStorage.getItem(TokenKey.REFRESH);
19// 存储新 Token
20const setTokens = (accessToken, refreshToken) => {
21  localStorage.setItem(TokenKey.ACCESS, accessToken);
22  localStorage.setItem(TokenKey.REFRESH, refreshToken);
23};
24// 清除 Token(退出登录用)
25const removeTokens = () => {
26  localStorage.removeItem(TokenKey.ACCESS);
27  localStorage.removeItem(TokenKey.REFRESH);
28};
29
30// 3. 刷新状态管理(防止并发请求重复刷新 Token)
31let isRefreshing = false; // 是否正在刷新 Token
32let requestQueue = []; // 等待刷新完成的请求队列
33
34// 4. 请求拦截器:自动给所有请求添加 Access Token
35service.interceptors.request.use(
36  (config) => {
37    const token = getAccessToken();
38    if (token) {
39      // 规范格式:Bearer + 空格 + Token(后端需对应解析)
40      config.headers.Authorization = `Bearer ${token}`;
41    }
42    return config;
43  },
44  (error) => Promise.reject(error)
45);
46
47// 5. 响应拦截器:处理 Token 过期逻辑
48service.interceptors.response.use(
49  (response) => response.data, // 直接返回响应体,简化业务层调用
50  async (error) => {
51    const { response, config } = error;
52    const originalRequest = config; // 原始失败请求
53
54    // 仅处理 401 状态码(Access Token 过期/无效),且排除刷新 Token 本身的请求
55    if (response?.status === 401 && originalRequest.url !== '/auth/refresh') {
56      // 避免重复刷新:正在刷新时,将请求加入队列
57      if (isRefreshing) {
58        return new Promise((resolve) => {
59          requestQueue.push(() => {
60            // 刷新成功后,用新 Token 重试原始请求
61            originalRequest.headers.Authorization = `Bearer ${getAccessToken()}`;
62            resolve(service(originalRequest));
63          });
64        });
65      }
66
67      originalRequest._retry = true; // 标记该请求已进入重试流程
68      isRefreshing = true; // 开启刷新状态
69
70      try {
71        // 调用后端刷新接口,用 Refresh Token 换取新 Token
72        const refreshToken = getRefreshToken();
73        if (!refreshToken) {
74          throw new Error('Refresh Token 不存在');
75        }
76
77        const refreshRes = await service.post('/auth/refresh', {
78          refreshToken, // 传给后端的 Refresh Token
79        });
80
81        // 存储新 Token
82        const { accessToken, refreshToken: newRefreshToken } = refreshRes;
83        setTokens(accessToken, newRefreshToken);
84
85        // 重试队列中所有等待的请求
86        requestQueue.forEach((callback) => callback());
87        requestQueue = []; // 清空队列
88
89        // 重试当前失败的请求
90        originalRequest.headers.Authorization = `Bearer ${accessToken}`;
91        return service(originalRequest);
92      } catch (refreshError) {
93        // 刷新失败(Refresh Token 过期/无效),强制跳转登录页
94        removeTokens(); // 清除本地无效 Token
95        ElMessage.error('登录已过期,请重新登录');
96        window.location.href = '/login'; // 跳转到登录页
97        return Promise.reject(refreshError);
98      } finally {
99        isRefreshing = false; // 关闭刷新状态
100      }
101    }
102
103    // 非 401 错误(如网络错误、业务错误),直接抛出
104    ElMessage.error(error.message || '请求失败');
105    return Promise.reject(error);
106  }
107);
108
109export default service;

2. 登录与业务请求示例(api/user.js)

1import service from './index';
2
3// 登录:获取初始双 Token
4export const login = (username, password) => {
5  return service.post('/auth/login', { username, password });
6};
7
8// 业务请求示例(无需手动处理 Token)
9export const getUserInfo = () => {
10  return service.get('/user/info');
11};
12
13// 退出登录:清除 Token
14export const logout = () => {
15  localStorage.removeItem('access_token');
16  localStorage.removeItem('refresh_token');
17  window.location.href = '/login';
18};

3. 登录页面使用示例(Login.vue)

1<template>
2  <div>
3    <input v-model="username" placeholder="用户名" />
4    <input v-model="password" type="password" placeholder="密码" />
5    <button @click="handleLogin">登录</button>
6  </div>
7</template>
8
9<script setup>
10import { ref } from 'vue';
11import { login } from '@/api/user';
12import { ElMessage } from 'element-plus';
13
14const username = ref('');
15const password = ref('');
16
17const handleLogin = async () => {
18  try {
19    // 调用登录接口,后端返回 accessToken 和 refreshToken
20    const res = await login(username.value, password.value);
21    // 存储 Token(实际已在 api 拦截器中处理,此处简化)
22    localStorage.setItem('access_token', res.accessToken);
23    localStorage.setItem('refresh_token', res.refreshToken);
24    ElMessage.success('登录成功');
25    window.location.href = '/home'; // 跳转到首页
26  } catch (error) {
27    ElMessage.error('登录失败,请检查账号密码');
28  }
29};
30</script>

三、后端实现(Node.js + Express)

1. 依赖安装

npm install express jsonwebtoken redis cors dotenv // 核心依赖

2. 核心配置(config.js)

1require('dotenv').config();
2
3module.exports = {
4  // JWT 密钥(生产环境需用环境变量,避免硬编码)
5  JWT_SECRET: process.env.JWT_SECRET || 'your-secret-key-321',
6  // Token 有效期
7  ACCESS_TOKEN_EXPIRES: '1h', // 1 小时
8  REFRESH_TOKEN_EXPIRES: '7d', // 7 天
9  // Redis 配置(存储 Refresh Token,防止重复使用)
10  REDIS: {
11    host: 'localhost',
12    port: 6379,
13    db: 0,
14  },
15};

3. JWT 工具函数(utils/jwt.js)

1const jwt = require('jsonwebtoken');
2const config = require('../config');
3
4// 生成 Token
5const generateToken = (payload, expiresIn) => {
6  return jwt.sign(payload, config.JWT_SECRET, { expiresIn });
7};
8
9// 验证 Token
10const verifyToken = (token) => {
11  try {
12    return jwt.verify(token, config.JWT_SECRET);
13  } catch (error) {
14    throw new Error('Token 无效或已过期');
15  }
16};
17
18module.exports = { generateToken, verifyToken };

4. Redis 工具函数(utils/redis.js)

1const redis = require('redis');
2const config = require('../config');
3
4// 创建 Redis 客户端
5const client = redis.createClient({
6  host: config.REDIS.host,
7  port: config.REDIS.port,
8  db: config.REDIS.db,
9});
10
11// 连接 Redis
12client.connect().catch((err) => console.error('Redis 连接失败:', err));
13
14// 存储 Refresh Token(key: userId, value: refreshToken)
15const setRefreshToken = async (userId, refreshToken) => {
16  // 有效期与 Refresh Token 一致(7 天)
17  await client.setEx(`refresh_token:${userId}`, 60 * 60 * 24 * 7, refreshToken);
18};
19
20// 获取 Refresh Token
21const getRefreshToken = async (userId) => {
22  return await client.get(`refresh_token:${userId}`);
23};
24
25// 删除 Refresh Token(退出登录时)
26const deleteRefreshToken = async (userId) => {
27  await client.del(`refresh_token:${userId}`);
28};
29
30module.exports = { setRefreshToken, getRefreshToken, deleteRefreshToken };

5. 核心接口实现(routes/auth.js)

1const express = require('express');
2const router = express.Router();
3const { generateToken, verifyToken } = require('../utils/jwt');
4const { setRefreshToken, getRefreshToken, deleteRefreshToken } = require('../utils/redis');
5const config = require('../config');
6
7// 模拟用户数据库(实际替换为 MySQL/MongoDB)
8const mockUsers = [
9  { id: 1, username: 'admin', password: '123456' },
10];
11
12// 1. 登录接口:生成双 Token
13router.post('/login', (req, res) => {
14  const { username, password } = req.body;
15  // 验证账号密码
16  const user = mockUsers.find(
17    (u) => u.username === username && u.password === password
18  );
19
20  if (!user) {
21    return res.status(400).json({ message: '账号或密码错误' });
22  }
23
24  // 生成双 Token(payload 中存储用户唯一标识,避免敏感信息)
25  const accessToken = generateToken({ userId: user.id }, config.ACCESS_TOKEN_EXPIRES);
26  const refreshToken = generateToken({ userId: user.id }, config.REFRESH_TOKEN_EXPIRES);
27
28  // 存储 Refresh Token 到 Redis(用于后续验证)
29  setRefreshToken(user.id, refreshToken);
30
31  // 返回双 Token 给前端
32  res.json({
33    code: 200,
34    message: '登录成功',
35    data: { accessToken, refreshToken },
36  });
37});
38
39// 2. 刷新 Token 接口:用有效 Refresh Token 换取新双 Token
40router.post('/refresh', async (req, res) => {
41  const { refreshToken } = req.body;
42  if (!refreshToken) {
43    return res.status(403).json({ message: 'Refresh Token 不能为空' });
44  }
45
46  try {
47    // 1. 验证 Refresh Token 有效性
48    const payload = verifyToken(refreshToken);
49    const { userId } = payload;
50
51    // 2. 验证 Redis 中存储的 Refresh Token 是否一致(防止伪造)
52    const storedRefreshToken = await getRefreshToken(userId);
53    if (storedRefreshToken !== refreshToken) {
54      return res.status(403).json({ message: 'Refresh Token 无效' });
55    }
56
57    // 3. 生成新的双 Token
58    const newAccessToken = generateToken({ userId }, config.ACCESS_TOKEN_EXPIRES);
59    const newRefreshToken = generateToken({ userId }, config.REFRESH_TOKEN_EXPIRES);
60
61    // 4. 更新 Redis 中的 Refresh Token(滑动过期,增强安全性)
62    await setRefreshToken(userId, newRefreshToken);
63
64    // 5. 返回新 Token
65    res.json({
66      code: 200,
67      data: { accessToken: newAccessToken, refreshToken: newRefreshToken },
68    });
69  } catch (error) {
70    return res.status(403).json({ message: 'Refresh Token 已过期,请重新登录' });
71  }
72});
73
74// 3. 退出登录接口:删除 Redis 中的 Refresh Token
75router.post('/logout', async (req, res) => {
76  const token = req.headers.authorization?.split(' ')[1];
77  if (!token) {
78    return res.status(400).json({ message: 'Token 不能为空' });
79  }
80
81  try {
82    const payload = verifyToken(token);
83    await deleteRefreshToken(payload.userId);
84    res.json({ code: 200, message: '退出登录成功' });
85  } catch (error) {
86    res.status(400).json({ message: '退出登录失败' });
87  }
88});
89
90module.exports = router;

6. 后端入口文件(app.js)

1const express = require('express');
2const cors = require('cors');
3const authRouter = require('./routes/auth');
4
5const app = express();
6const port = 3001;
7
8// 跨域配置(生产环境需限制 origin)
9app.use(cors());
10// 解析 JSON 请求体
11app.use(express.json());
12
13// 挂载路由
14app.use('/api/auth', authRouter);
15
16// 启动服务
17app.listen(port, () => {
18  console.log(`后端服务启动成功:http://localhost:${port}`);
19});

https://mybj123.com/27766.html

四、关键注意事项(生产环境必看)

  1. 安全存储 Token
    • 不推荐用 localStorage 存储(易受 XSS 攻击),优先用 HttpOnly Cookie 存储 Refresh Token,前端无法读取,避免窃取。
    • Access Token 可存在内存(如 Vuex/Pinia),页面刷新后通过 Cookie 获取 Refresh Token 重新刷新。
  2. 防止重复刷新
    • isRefreshing状态和requestQueue队列,避免多个并发请求同时触发刷新接口,导致 Token 冲突。
  3. Redis 的必要性
    • 存储 Refresh Token 到 Redis,支持 “强制登出”“单点登录” 功能(如修改密码后,删除 Redis 中的旧 Refresh Token,强制用户重新登录)。
  4. HTTPS 协议
    • 生产环境必须启用 HTTPS,防止 Token 在传输过程中被中间人窃取。
  5. Token 有效期合理设置
    • Access Token:15 分钟~2 小时(越短越安全)。
    • Refresh Token:7~30 天(平衡安全性和用户体验)。

五、完整流程梳理

  1. 用户登录 → 后端验证账号密码 → 返回 Access Token 和 Refresh Token → 前端存储。
  2. 前端发起业务请求 → 拦截器自动携带 Access Token → 后端验证有效 → 返回业务数据。
  3. 若 Access Token 过期 → 后端返回 401 → 前端拦截器调用刷新接口。
  4. 刷新接口验证 Refresh Token 有效 → 返回新双 Token → 前端更新存储,重试原始请求。
  5. 若 Refresh Token 过期 → 前端清除 Token,跳转登录页。

Vue + Axios + Node.js(Express)如何实现无感刷新Token?》 是转载文章,点击查看原文


相关推荐


如何使用 Spec Kit 工具进行规范驱动开发?
磊磊落落2025/11/18

大家好,我是磊磊落落,目前我在技术上主要关注:Java、Golang、AI、架构设计、云原生和自动化测试。欢迎来我的博客(leileiluoluo.com)获取我的最近更新! 由上文「Markdown 将成为 AI 时代的通用编程语言?」可以知道,规范驱动开发可能成为 AI 时代的开发新范式。 在传统软件开发流程中,规范只是编码前的临时脚手架,开发者一旦进入编码阶段,便将规范束之高阁。而进入 AI 时代,「规范驱动开发」想彻底改变这一现状,即让规范贯穿整个软件开发生命周期、让规范变得可执行、让


FPGA工程师12实战项目-基于PCIe的高速ADC采集项目
第二层皮-合肥2025/11/17

目录 简介 项目内容 项目内容 实战内容 最后做总结 简介 最近新凯莱的高速示波器项目很火爆,于是计划做一高速示波器的实战项目,由于硬件电路设计已经安排了,在同步安排一篇关于FPGA的。(计划教学5名学员) 项目内容 本方案基于XINLINX的K7系列FPGA,ADC选用AD9226。 项目内容 FPGA段固件程序:负责采集前端ADC的信号,FPGA基本框架,数据协议 PCIe卡驱动:负责上位机测试程序与PCie采集卡的数据交互 PC段测试程序:


零信任架构下的 WebAIGC 服务安全技术升级方向
LeonGao2025/11/16

前言:我们已不再“相信”一切 在互联网江湖的旧时代,安全防线的哲学像是一座古城门: “只要进了城,全是自己人;只要在外面,全是坏人。” 这种“城内无敌”的逻辑简单粗暴,但当我们的 应用、用户和AI模型 分散到全球各地的云端节点上时,城门的概念变得像《三体》的面壁计划——看似防御,实则透明。 于是,新时代的口号变成了: 零信任(Zero Trust)——默认无信任,一切验证重启。 一、零信任理念的内核哲学 如果把计算系统比作一个社会,那么“零信任”就像是一个反乌托邦的理性国度: 公民(


[Unity Shader Base] RayMarching in Cloud Rendering
一步一个foot-print2025/11/14

基础知识: 1.SDF 有符号距离场,且通过正负可以判断在物体外部还是内部,通常外正内负 这是RayMarching的灵魂支撑,能够通过一个数学函数,输入一个空间中的点,输出这个点到物体表面的最短距离(带符号)。可以使复杂的几何形状可以通过简单的 SDF运算来组合。比如,两个球体的 SDF 可以通过 min() 操作来融合,通过 max() 来相交,通过 abs()和减法来创造出“镂空”效果。 RayMarching 区别于正常的射线求交,根据他的中文翻译名,光线步进,可以比较生动


DNS正反向解析&转发服务器&主从服务
firstacui2025/11/13

DNS正反向解析&转发服务器&主从服务 1. 正反向解析 主机角色系统IPclient客户端redhat 9.6192.168.72.7server域名解析服务器redhat 9.6192.168.72.181.1 配置服务端 1)修改主机名和IP地址 [root@localhost ~]# hostnamectl hostname server [root@server ~]# nmcli c m ens160 ipv4.addresses 192.168.72.18/24 [root@s


CV论文速递:覆盖视频理解与生成、跨模态与定位、医学与生物视觉、图像数据集等方向(11.03-11.07)
CV实验室2025/11/11

本周精选12篇CV领域前沿论文,覆盖视频理解与生成、跨模态与定位、医学与生物视觉、图像数据集与模型优化等方向。全部200多篇论文感兴趣的自取! 原文 资料 这里! 一、视频理解与生成方向 1、Cambrian-S: Towards Spatial Supersensing in Video 作者:Shusheng Yang, Jihan Yang, Pinzhi Huang, Ellis Brown, Zihao Yang, Yue Yu, Shengbang Tong,


软考 系统架构设计师之考试感悟4
蓝天居士2025/11/10

接前一篇文章:软考 系统架构设计师之考试感悟3 昨天(2025年11月8日),本人第四次参加了软考系统架构师的考试。和前三次一样,考了一天,身心俱疲。这次感觉和上一次差不多,考的次数多了,也就习惯了。仍然有诸多感悟,下边将本次参加考试的感悟写在这里,以资自己及后来者借鉴。 上一次参加考试是今年的5月24号,地点还是前两次那个地方(本次也是) —— 北京市商业学院(远大路校区),坐公交只需要30分钟、骑车只需要15分钟左右。上次考试结果是在今年的6月26号、即考试后的一个月左右的时间出的。


C++:类和对象---进阶篇
仟千意2025/11/8

1. 类的默认成员函数 默认成员函数就是我们没有显式实现,C++会自动生成的成员函数称为默认成员函数,C++11后,C++类的默认成员函数有8个(默认构造函数、默认析构函数、拷贝构造函数、赋值运算符重载、取地址运算符重载、const取地址运算符重载、移动构造函数(C++11后)、移动赋值运算符重载(C++11后)),我们此文只了解重要的前4个,后4个中前两个不常用,后两个之后再做讲解。 2. 构造函数 构造函数是特殊的成员函数,虽名为构造,但它完成的是成员变量的初始化工作,所以它可以完美的


90%前端面试必问的12个JS核心,搞懂这些直接起飞!
良山有风来2025/11/5

你是不是也遇到过这样的场景?面试官抛出一个闭包问题,你支支吾吾答不上来;团队代码review时,看到同事用的Promise链一脸懵逼;明明功能实现了,性能却总是差那么一点... 别慌!今天我整理了12个JavaScript核心概念,这些都是2024年各大厂面试的高频考点,也是日常开发中真正实用的硬核知识。搞懂它们,不仅能轻松应对面试,更能让你的代码质量提升一个档次! 变量与作用域 先来看个最常见的面试题: // 经典面试题:猜猜输出什么? for (var i = 0; i < 3; i++)


OpenAI Aardvark:当AI化身代码守护者
墨风如雪2025/10/31

想象一下,一个不知疲倦、聪明绝顶的数字侦探,夜以继日地巡视你的代码,在每一个新提交、每一行变更中嗅探潜在的危险。这不是科幻,而是OpenAI在2025年末悄然放出的重磅炸弹——Aardvark。这款以“土豚”命名的AI智能体,并非简单的代码扫描器,它标志着AI在网络安全领域,真正迈出了“自主思考”的第一步。 认识你的新安全伙伴 Aardvark,由OpenAI最先进的GPT-5模型驱动,被定位为一个“agentic security researcher”。你可以把它理解为一位全职的“白帽黑客

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0