Token无感刷新全流程(Vue + Axios + Node.js(Express))

2025-12-04 0 788

页面基本流程

  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换取新Token
        const refreshToken = getRefreshToken();
        if (!refreshToken) {
          throw new Error(\'Refresh Token不存在\');
        }

        const refreshRes = await service.post(\'/auth/refresh\', {
          refreshToken, // 传给后端的Refresh Token
        });

        // 存储新Token
        const { 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(); // 清除本地无效Token
        ElMessage.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和refreshToken
    const 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)

javascript

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. 生成新的双Token
    const newAccessToken = generateToken({ userId }, config.ACCESS_TOKEN_EXPIRES);
    const newRefreshToken = generateToken({ userId }, config.REFRESH_TOKEN_EXPIRES);

    // 4. 更新Redis中的Refresh Token(滑动过期,增强安全性)
    await setRefreshToken(userId, newRefreshToken);

    // 5. 返回新Token
    res.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}`);
});

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

  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,跳转登录页。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 Token无感刷新全流程(Vue + Axios + Node.js(Express)) https://www.zuozi.net/3605.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务