当前位置: 首页 > news >正文

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

在前后端分离架构中,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 携带、刷新和重试逻辑:、

import axios from 'axios';
import { ElMessage } from 'element-plus'; // 按需引入 UI 组件库提示(可选)// 1. 创建 Axios 实例
const service = axios.create({baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量配置后端地址timeout: 5000, // 请求超时时间
});// 2. Token 存取工具函数(安全存储建议用 HttpOnly Cookie,此处用 localStorage 演示)
const TokenKey = {ACCESS: 'access_token',REFRESH: 'refresh_token',
};// 获取 Token
const getAccessToken = () => localStorage.getItem(TokenKey.ACCESS);
const getRefreshToken = () => localStorage.getItem(TokenKey.REFRESH);
// 存储新 Token
const setTokens = (accessToken, refreshToken) => {localStorage.setItem(TokenKey.ACCESS, accessToken);localStorage.setItem(TokenKey.REFRESH, refreshToken);
};
// 清除 Token(退出登录用)
const removeTokens = () => {localStorage.removeItem(TokenKey.ACCESS);localStorage.removeItem(TokenKey.REFRESH);
};// 3. 刷新状态管理(防止并发请求重复刷新 Token)
let isRefreshing = false; // 是否正在刷新 Token
let requestQueue = []; // 等待刷新完成的请求队列// 4. 请求拦截器:自动给所有请求添加 Access Token
service.interceptors.request.use((config) => {const token = getAccessToken();if (token) {// 规范格式:Bearer + 空格 + Token(后端需对应解析)config.headers.Authorization = `Bearer ${token}`;}return config;},(error) => Promise.reject(error)
);// 5. 响应拦截器:处理 Token 过期逻辑
service.interceptors.response.use((response) => response.data, // 直接返回响应体,简化业务层调用async (error) => {const { response, config } = error;const originalRequest = config; // 原始失败请求// 仅处理 401 状态码(Access Token 过期/无效),且排除刷新 Token 本身的请求if (response?.status === 401 && originalRequest.url !== '/auth/refresh') {// 避免重复刷新:正在刷新时,将请求加入队列if (isRefreshing) {return new Promise((resolve) => {requestQueue.push(() => {// 刷新成功后,用新 Token 重试原始请求originalRequest.headers.Authorization = `Bearer ${getAccessToken()}`;resolve(service(originalRequest));});});}originalRequest._retry = true; // 标记该请求已进入重试流程isRefreshing = true; // 开启刷新状态try {// 调用后端刷新接口,用 Refresh Token 换取新 Tokenconst refreshToken = getRefreshToken();if (!refreshToken) {throw new Error('Refresh Token 不存在');}const refreshRes = await service.post('/auth/refresh', {refreshToken, // 传给后端的 Refresh Token});// 存储新 Tokenconst { accessToken, refreshToken: newRefreshToken } = refreshRes;setTokens(accessToken, newRefreshToken);// 重试队列中所有等待的请求requestQueue.forEach((callback) => callback());requestQueue = []; // 清空队列// 重试当前失败的请求originalRequest.headers.Authorization = `Bearer ${accessToken}`;return service(originalRequest);} catch (refreshError) {// 刷新失败(Refresh Token 过期/无效),强制跳转登录页removeTokens(); // 清除本地无效 TokenElMessage.error('登录已过期,请重新登录');window.location.href = '/login'; // 跳转到登录页return Promise.reject(refreshError);} finally {isRefreshing = false; // 关闭刷新状态}}// 非 401 错误(如网络错误、业务错误),直接抛出ElMessage.error(error.message || '请求失败');return Promise.reject(error);}
);export default service;

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

import service from './index';// 登录:获取初始双 Token
export const login = (username, password) => {return service.post('/auth/login', { username, password });
};// 业务请求示例(无需手动处理 Token)
export const getUserInfo = () => {return service.get('/user/info');
};// 退出登录:清除 Token
export const logout = () => {localStorage.removeItem('access_token');localStorage.removeItem('refresh_token');window.location.href = '/login';
};

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

<template><div><input v-model="username" placeholder="用户名" /><input v-model="password" type="password" placeholder="密码" /><button @click="handleLogin">登录</button></div>
</template><script setup>
import { ref } from 'vue';
import { login } from '@/api/user';
import { ElMessage } from 'element-plus';const username = ref('');
const password = ref('');const handleLogin = async () => {try {// 调用登录接口,后端返回 accessToken 和 refreshTokenconst res = await login(username.value, password.value);// 存储 Token(实际已在 api 拦截器中处理,此处简化)localStorage.setItem('access_token', res.accessToken);localStorage.setItem('refresh_token', res.refreshToken);ElMessage.success('登录成功');window.location.href = '/home'; // 跳转到首页} catch (error) {ElMessage.error('登录失败,请检查账号密码');}
};
</script>

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

1. 依赖安装

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

2. 核心配置(config.js)

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

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

const jwt = require('jsonwebtoken');
const config = require('../config');// 生成 Token
const generateToken = (payload, expiresIn) => {return jwt.sign(payload, config.JWT_SECRET, { expiresIn });
};// 验证 Token
const verifyToken = (token) => {try {return jwt.verify(token, config.JWT_SECRET);} catch (error) {throw new Error('Token 无效或已过期');}
};module.exports = { generateToken, verifyToken };

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

const redis = require('redis');
const config = require('../config');// 创建 Redis 客户端
const client = redis.createClient({host: config.REDIS.host,port: config.REDIS.port,db: config.REDIS.db,
});// 连接 Redis
client.connect().catch((err) => console.error('Redis 连接失败:', err));// 存储 Refresh Token(key: userId, value: refreshToken)
const setRefreshToken = async (userId, refreshToken) => {// 有效期与 Refresh Token 一致(7 天)await client.setEx(`refresh_token:${userId}`, 60 * 60 * 24 * 7, refreshToken);
};// 获取 Refresh Token
const getRefreshToken = async (userId) => {return await client.get(`refresh_token:${userId}`);
};// 删除 Refresh Token(退出登录时)
const deleteRefreshToken = async (userId) => {await client.del(`refresh_token:${userId}`);
};module.exports = { setRefreshToken, getRefreshToken, deleteRefreshToken };

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

const express = require('express');
const router = express.Router();
const { generateToken, verifyToken } = require('../utils/jwt');
const { setRefreshToken, getRefreshToken, deleteRefreshToken } = require('../utils/redis');
const config = require('../config');// 模拟用户数据库(实际替换为 MySQL/MongoDB)
const mockUsers = [{ id: 1, username: 'admin', password: '123456' },
];// 1. 登录接口:生成双 Token
router.post('/login', (req, res) => {const { username, password } = req.body;// 验证账号密码const user = mockUsers.find((u) => u.username === username && u.password === password);if (!user) {return res.status(400).json({ message: '账号或密码错误' });}// 生成双 Token(payload 中存储用户唯一标识,避免敏感信息)const accessToken = generateToken({ userId: user.id }, config.ACCESS_TOKEN_EXPIRES);const refreshToken = generateToken({ userId: user.id }, config.REFRESH_TOKEN_EXPIRES);// 存储 Refresh Token 到 Redis(用于后续验证)setRefreshToken(user.id, refreshToken);// 返回双 Token 给前端res.json({code: 200,message: '登录成功',data: { accessToken, refreshToken },});
});// 2. 刷新 Token 接口:用有效 Refresh Token 换取新双 Token
router.post('/refresh', async (req, res) => {const { refreshToken } = req.body;if (!refreshToken) {return res.status(403).json({ message: 'Refresh Token 不能为空' });}try {// 1. 验证 Refresh Token 有效性const payload = verifyToken(refreshToken);const { userId } = payload;// 2. 验证 Redis 中存储的 Refresh Token 是否一致(防止伪造)const storedRefreshToken = await getRefreshToken(userId);if (storedRefreshToken !== refreshToken) {return res.status(403).json({ message: 'Refresh Token 无效' });}// 3. 生成新的双 Tokenconst newAccessToken = generateToken({ userId }, config.ACCESS_TOKEN_EXPIRES);const newRefreshToken = generateToken({ userId }, config.REFRESH_TOKEN_EXPIRES);// 4. 更新 Redis 中的 Refresh Token(滑动过期,增强安全性)await setRefreshToken(userId, newRefreshToken);// 5. 返回新 Tokenres.json({code: 200,data: { accessToken: newAccessToken, refreshToken: newRefreshToken },});} catch (error) {return res.status(403).json({ message: 'Refresh Token 已过期,请重新登录' });}
});// 3. 退出登录接口:删除 Redis 中的 Refresh Token
router.post('/logout', async (req, res) => {const token = req.headers.authorization?.split(' ')[1];if (!token) {return res.status(400).json({ message: 'Token 不能为空' });}try {const payload = verifyToken(token);await deleteRefreshToken(payload.userId);res.json({ code: 200, message: '退出登录成功' });} catch (error) {res.status(400).json({ message: '退出登录失败' });}
});module.exports = router;

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

const express = require('express');
const cors = require('cors');
const authRouter = require('./routes/auth');const app = express();
const port = 3001;// 跨域配置(生产环境需限制 origin)
app.use(cors());
// 解析 JSON 请求体
app.use(express.json());// 挂载路由
app.use('/api/auth', authRouter);// 启动服务
app.listen(port, () => {console.log(`后端服务启动成功:http://localhost:${port}`);
});

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,跳转登录页。
http://www.dtcms.com/a/609516.html

相关文章:

  • 重大更新!Ubuntu Pro 现提供长达 15 年的安全支持
  • 重庆做学校网站公司农村服务建设有限公司网站
  • 尝试本地部署 Stable Diffusion
  • 网站前置审批专项好的用户体验网站
  • 【动规】背包问题
  • js:网页屏幕尺寸小于768时,切换到移动端页面
  • 《LLM零开销抽象与插件化扩展指南》
  • C++_面试题_21_字符串操作
  • 多重组合问题与矩阵配额问题
  • 什么情况下会把 SYN 包丢弃?
  • EG27324 带关断功能双路MOS驱动芯片技术解析
  • do_action wordpress 模板关键词优化排名的步骤
  • 海外网站入口通信管理局 网站备案
  • 在 Java 中实现 Excel 数字与文本转换
  • 如何保持不同平台的体验一致性
  • redis(五)——管道、主从复制
  • OBS直播教程:OBS实时字幕插件如何下载?OBS实时字幕插件如何安装?OBS实时字幕插件如何使用?OBS实时字幕插件官方下载地址
  • WPF中TemplatePart机制详解
  • 大学生毕业设计课题做网站网站开发研发设计
  • PPT制作正在发生一场静默革命
  • 无线通信信道的衰落特性
  • 大模型量化压缩实战:从FP16到INT4的生产级精度保持之路
  • ListDLLs Handle 学习笔记(8.11):谁注入了 DLL?谁占着文件不放?一篇教你全搞定
  • 电子电气架构 ---软件架构的准则与描述
  • linux下网站搭建wordpress文章页图片尺寸
  • 上海集团网站建设公司好蚌埠的网站建设
  • opencv 学习: QA_01 什么是图像锐化
  • C++标准库中的排序算法
  • 做网站图片和文字字体侵权seo是什么意思金融
  • Node.js npm 安装过程中 EBUSY 错误的分析与解决方案