双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完整流程:从请求到无感刷新
先看一张“闭环流程图”,明白整个机制的核心逻辑:
- 发起请求:前端接口自动携带Access Token;
- 401拦截:Access Token过期,后端返回401;
- 状态判断:若未在刷新中(isRefreshing=false),锁定刷新流程;
- 刷新Token:用Refresh Token调用刷新接口,获取新Access Token;
- 重试请求:用新Token重试当前失败的请求;
- 队列处理:期间其他401请求加入队列,统一用新Token重试;
- 状态重置:清空队列、解锁刷新,无感续期完成;
- 彻底过期:若Refresh Token无效,跳转登录页。
三、实战:Vue3 + Axios 实现双Token(附完整代码)
以 Vue3 为例(React 仅需替换状态管理库,逻辑完全一致),分4步落地。
1. 第一步:登录获取双Token,安全存储
用户登录后,后端返回 accessToken
和 refreshToken
,关键是区分存储方式,从源头防漏洞。
代码实现(登录逻辑: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个点
-
防XSS攻击
- 始终将Refresh Token存
HttpOnly Cookie
,前端无法读取; - Access Token存内存(Vuex/Redux),刷新页面后消失,即使被XSS窃取,有效期也只有2小时。
- 始终将Refresh Token存
-
防CSRF攻击
- Cookie加
SameSite=Strict
:仅同域请求携带Cookie,防止跨站伪造; - 后端可额外加CSRF Token:在刷新接口中验证,双重防护。
- Cookie加
-
提前刷新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(); });
五、体验优化:让用户完全无感知
- 全局加载状态:刷新Token时显示遮罩,避免用户重复点击;
- 登录后返回原页面:通过
redirect
参数,登录后跳转回之前的页面; - 静态资源跳过Token:图片、CSS等无需认证,减少请求头体积,优化性能。
六、总结:双Token的核心价值
双Token不是“复杂技术”,而是“体验与安全的平衡术”:
- 对用户:一次登录,7天内无需重复输密码,体验流畅;
- 对开发者:通过“内存+HttpOnly Cookie”存储、并发控制、提前刷新,筑牢安全防线;
- 对系统:短期Access Token降低泄露风险,长期Refresh Token减少服务器压力。
如果你正在开发网页端单页应用(SPA),这套双Token方案绝对是身份认证的首选——按本文步骤实战,1小时就能搭建起“无感刷新+高安全”的认证体系!