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

双Token实战:从无感刷新到安全防护,完整流程+代码解析

你是否常被这些问题困扰?

  • 用户吐槽“刚登录就过期,反复输密码太麻烦”;
  • 担心“Token存在localStorage,被XSS偷了怎么办”;
  • 调试时遇到“多个接口同时401,重复刷新Token乱成一锅粥”。

别担心,双Token机制(Access Token + Refresh Token) 正是为解决这些痛点而生。今天结合 Vue3/React + Axios 实战,带你从“原理→流程→代码→避坑”,搭建一套“登录一次,无感续期”的身份认证体系,兼顾安全与体验。

一、先搞懂:为什么网页端必须用双Token?

单Token方案的“两难困境”,是双Token存在的核心原因:

单Token痛点双Token解决方案
有效期短→用户频繁登录Access Token(短期2小时)+ Refresh Token(长期7天)
有效期长→泄露风险高Access Token仅存内存,Refresh Token安全存HttpOnly Cookie
无法兼顾“体验”与“安全”日常用Access Token,续期用Refresh Token,各司其职

双Token的核心分工,一句话讲透:

Access Token是“临时门禁卡”:用于接口请求,过期快、风险低;
Refresh Token是“长期通行证”:仅用于刷新门禁卡,安全存储、不直接参与接口访问。

二、网页端双Token完整流程:从请求到无感刷新

先看一张“闭环流程图”,明白整个机制的核心逻辑:

  1. 发起请求:前端接口自动携带Access Token;
  2. 401拦截:Access Token过期,后端返回401;
  3. 状态判断:若未在刷新中(isRefreshing=false),锁定刷新流程;
  4. 刷新Token:用Refresh Token调用刷新接口,获取新Access Token;
  5. 重试请求:用新Token重试当前失败的请求;
  6. 队列处理:期间其他401请求加入队列,统一用新Token重试;
  7. 状态重置:清空队列、解锁刷新,无感续期完成;
  8. 彻底过期:若Refresh Token无效,跳转登录页。

三、实战:Vue3 + Axios 实现双Token(附完整代码)

以 Vue3 为例(React 仅需替换状态管理库,逻辑完全一致),分4步落地。

1. 第一步:登录获取双Token,安全存储

用户登录后,后端返回 accessTokenrefreshToken,关键是区分存储方式,从源头防漏洞。

代码实现(登录逻辑:src/api/auth.js)
import axios from 'axios';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';// 登录函数
export async function login(username, password) {const store = useStore();const router = useRouter();try {// 1. 调用后端登录接口const res = await axios.post('/api/auth/login', { username, password });const { accessToken, refreshToken } = res.data;// 2. Access Token存Vuex(内存):刷新页面消失,防XSSstore.commit('auth/setAccessToken', accessToken);// 3. Refresh Token存HttpOnly Cookie:前端无法读取,安全等级最高// 生产环境必须加 Secure(仅HTTPS)、SameSite(防CSRF)document.cookie = `refreshToken=${refreshToken}; HttpOnly; Secure=${process.env.NODE_ENV === 'production'}; SameSite=Strict; path=/; max-age=${7 * 24 * 60 * 60}`; // 7天有效期// 4. 跳转首页router.push('/home');} catch (err) {alert(err.response?.data?.msg || '登录失败,请检查账号密码');}
}
存储安全重点:
  • ❌ 禁止把Access Token存localStorage/sessionStorage:易被XSS脚本窃取;
  • ✅ Refresh Token必须加 HttpOnly:前端JS无法访问,彻底杜绝XSS窃取风险;
  • ✅ 生产环境加 Secure:仅通过HTTPS传输Cookie,防止中途被拦截。

2. 第二步:请求拦截,自动携带Access Token

用Axios请求拦截器,给所有需要认证的接口“自动贴Token”,不用手动写请求头。

代码实现(Axios封装:src/utils/request.js)
import axios from 'axios';
import { useStore } from 'vuex';// 创建Axios实例
const request = axios.create({baseURL: '/api',timeout: 5000
});// 请求拦截器:添加Token
request.interceptors.request.use((config) => {const store = useStore();const accessToken = store.state.auth.accessToken;// 仅给“非 auth 接口”加Token(排除登录、刷新接口)if (accessToken && !config.url.includes('/api/auth/')) {config.headers.Authorization = `Bearer ${accessToken}`; // 符合HTTP认证规范}// 静态资源(图片、CSS)跳过Token(优化性能)if (config.url.match(/\.(png|jpg|jpeg|css|js)$/)) {delete config.headers.Authorization;}return config;},(error) => Promise.reject(error)
);export default request;

3. 第三步:响应拦截,实现无感刷新(核心)

当Access Token过期(后端返回401),用Refresh Token悄悄刷新,关键是解决“并发刷新”问题(多个接口同时401,避免重复调用刷新接口)。

代码实现(响应拦截器:src/utils/request.js 续)
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';// 全局变量:控制并发刷新
let isRefreshing = false; // 刷新锁:防止重复刷新
let requestQueue = [];    // 请求队列:存储等待刷新的请求// 响应拦截器:处理401
request.interceptors.response.use((response) => response,async (error) => {const originalRequest = error.config;const store = useStore();const router = useRouter();// 非401错误,直接抛出if (error.response?.status !== 401) {return Promise.reject(error);}// 已重试过的请求,避免无限循环if (originalRequest._retry) {return Promise.reject(error);}// 标记为已重试originalRequest._retry = true;try {// 场景1:正在刷新Token,当前请求加入队列if (isRefreshing) {return new Promise((resolve) => {requestQueue.push((newToken) => {// 刷新成功后,用新Token重试originalRequest.headers.Authorization = `Bearer ${newToken}`;resolve(request(originalRequest));});});}// 场景2:未在刷新,开始刷新流程isRefreshing = true; // 加锁store.commit('app/setLoading', true); // 显示全局加载(优化体验)// 1. 获取Refresh Token(前端无法读HttpOnly Cookie,需后端配合自动获取)// 实际项目中:后端直接从Cookie读refreshToken,前端无需传参const res = await axios.post('/api/auth/refresh');const newAccessToken = res.data.accessToken;// 2. 保存新Token到Vuexstore.commit('auth/setAccessToken', newAccessToken);// 3. 重试队列中所有请求requestQueue.forEach((callback) => callback(newAccessToken));requestQueue = []; // 清空队列// 4. 重试当前请求originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;return request(originalRequest);} catch (refreshError) {// 刷新失败(Refresh Token过期):彻底登出store.commit('auth/clearAccessToken'); // 清除Access Tokendocument.cookie = 'refreshToken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; // 清除Cookie// 携带当前路径,登录后返回原页面(优化体验)router.push(`/login?redirect=${encodeURIComponent(router.currentRoute.value.path)}`);return Promise.reject(refreshError);} finally {// 解锁 + 隐藏加载isRefreshing = false;store.commit('app/setLoading', false);}}
);
核心逻辑解析:
  • 并发控制isRefreshing 加锁,确保同一时间只有一个刷新请求;
  • 队列重试requestQueue 存储等待的请求,刷新成功后统一重试,用户无感知;
  • 后端配合:刷新接口无需前端传Refresh Token,后端直接从Cookie读取,更安全。

4. 第四步:登出清理,彻底失效Token

用户主动登出时,不仅要清除前端存储,还要通知后端让Refresh Token失效,防止被复用。

代码实现(登出逻辑:src/api/auth.js 续)
export async function logout() {const store = useStore();const router = useRouter();try {// 关键:调用后端登出接口,让Refresh Token在服务器端失效await axios.post('/api/auth/logout');} catch (err) {console.error('登出接口失败(不影响前端清理)', err);} finally {// 清除前端存储store.commit('auth/clearAccessToken');document.cookie = 'refreshToken=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';// 跳转登录页router.push('/login');}
}

四、避坑指南:网页端双Token必注意的3个点

  1. 防XSS攻击

    • 始终将Refresh Token存 HttpOnly Cookie,前端无法读取;
    • Access Token存内存(Vuex/Redux),刷新页面后消失,即使被XSS窃取,有效期也只有2小时。
  2. 防CSRF攻击

    • Cookie加 SameSite=Strict:仅同域请求携带Cookie,防止跨站伪造;
    • 后端可额外加CSRF Token:在刷新接口中验证,双重防护。
  3. 提前刷新Token
    不要等401再刷新!解析Access Token的 exp 字段(如JWT),过期前30秒主动刷新,减少重试耗时:

    // 判断Token是否即将过期(剩余<30秒)
    export function isTokenExpiring(accessToken) {if (!accessToken) return true;const payload = JSON.parse(atob(accessToken.split('.')[1])); // 解析JWT payloadconst expTime = payload.exp * 1000; // 过期时间戳(秒转毫秒)return expTime - Date.now() < 30 * 1000;
    }// 在路由守卫中主动刷新
    router.beforeEach(async (to, from, next) => {const store = useStore();const accessToken = store.state.auth.accessToken;if (accessToken && isTokenExpiring(accessToken)) {await axios.post('/api/auth/refresh'); // 主动刷新}next();
    });
    

五、体验优化:让用户完全无感知

  1. 全局加载状态:刷新Token时显示遮罩,避免用户重复点击;
  2. 登录后返回原页面:通过 redirect 参数,登录后跳转回之前的页面;
  3. 静态资源跳过Token:图片、CSS等无需认证,减少请求头体积,优化性能。

六、总结:双Token的核心价值

双Token不是“复杂技术”,而是“体验与安全的平衡术”:

  • 对用户:一次登录,7天内无需重复输密码,体验流畅;
  • 对开发者:通过“内存+HttpOnly Cookie”存储、并发控制、提前刷新,筑牢安全防线;
  • 对系统:短期Access Token降低泄露风险,长期Refresh Token减少服务器压力。

如果你正在开发网页端单页应用(SPA),这套双Token方案绝对是身份认证的首选——按本文步骤实战,1小时就能搭建起“无感刷新+高安全”的认证体系!

http://www.xdnf.cn/news/1435933.html

相关文章:

  • PostgreSQL(1) FETCH用法
  • 【MySQL体系结构详解:一条SQL查询的旅程】
  • 《一篇拿下!C++:类和对象(中)构造函数与析构函数》
  • Java 21 虚拟线程 + 分布式调度深度实战:从原理到落地,大促日志同步效率提升 367%
  • 基于SpringBoot的校园资料分享平台
  • Mysql数据库基础(上)
  • 第1章:VisualVM 简介与安装
  • 东土科技战略升级:成立半导体子公司,赋能国产半导体智能化升级
  • 基于 HTML、CSS 和 JavaScript 的智能图像锐化系统
  • HTML第五课:求职登记表
  • 【实时Linux实战系列】基于实时Linux的农业自动化系统开发
  • C++ numeric库简介与使用指南
  • 项目解析:技术实现与面试高频问题
  • Linux - 进程切换
  • Git在idea中的实战使用经验(一)
  • 【TRAE调教指南之MCP篇】Exa MCP:治疗AI幻觉的有效方案
  • 构建企业级区块链网络:基于AWS EC2的弹性、高可用解决方案
  • CICD 持续集成与持续交付
  • GDB 调试
  • 第4章:内存分析与堆转储
  • 命令行文本处理小工具:cut、sort、uniq、tr 详解与应用
  • EMQX 4.4 加mysql认证
  • BandiZip下载与详细图文安装教程!!
  • docker 安装 redis 并设置 volumes 并修改 修改密码(二)
  • 构建可扩展的 AI 应用:LangChain 与 MCP 服务的集成模式
  • C++算法学习:位运算
  • ECMWF数据批量下载(Windows版本)
  • Ngene:实验设计的尖端利器
  • 洛谷P3811 【模板】模意义下的乘法逆元
  • Linux操作系统(6)