前言
在前后端分离的项目中,为了安全,Token 通常会设置有效期。但如果 Token 过期时强制用户重新登录,会极大地破坏用户体验。如何做到在用户毫无察觉的情况下,自动完成 Token 的续期?本文将深度拆解 “双 Token 无感刷新” 的实现机制。
一、 为什么需要“无感刷新”?
举个简单例子,你正在某 App 编辑内容,中途切出几分钟,再切回来时,直接弹出登录页,提示“登录已过期,请重新登录”,这种场景很容易让用户流失。
传统的单 Token 方案存在一个两难境地:
- 有效期过短:用户操作频繁,动不动就跳回登录页,用户体验极差。
- 有效期过长:Token 一旦被截获,风险极高。
解决方案:双 Token 机制
- access_token:访问令牌。有效期短(如 1 小时),每次接口请求都携带,降低泄露风险。
- refresh_token:刷新令牌。有效期长(如 7 天),仅用于
access_token过期时换取新令牌。
只要用户在 7 天内活跃过,系统就能通过 refresh_token 自动“续命”,实现长效无感登录。
二、 核心流程设计
- 正常请求:前端携带
access_token访问。 - 触发过期:后端返回 401 Unauthorized。
- 判断逻辑:
- 如果是普通接口报 401:说明
access_token失效,尝试刷新。 - 如果是刷新接口报 401:说明
refresh_token也失效了,强制重新登录。
- 如果是普通接口报 401:说明
- 无感替换:前端自动调用刷新接口,获取新 Token 覆盖本地存储,并重新发起之前失败的请求。
三、 细节攻坚:如何处理并发请求?
痛点:如果页面同时发出了 5 个请求,而此时 Token 刚好过期,会导致这 5 个请求同时触发“刷新 Token”的操作,造成资源浪费甚至后端异常。
解决策略:
- 状态锁 (
refreshing) :记录当前是否正在刷新中。 - 任务队列 (
queue) :在刷新期间到达的请求,先暂存起来,不直接报错。 - 批量回放:等待 Token 刷新成功后,依次执行队列里的请求,实现“无感”衔接。
四、 代码实现 (Axios 拦截器)
以下是基于 Axios 的完整工程化实现:
1import axios, { AxiosRequestConfig } from 'axios'; 2 3interface PendingTask { 4 config: AxiosRequestConfig; 5 resolve: Function; 6} 7 8let refreshing = false; // 状态锁:标志是否正在刷新 Token 9let queue: PendingTask[] = []; // 请求队列:暂存 Token 刷新期间的请求 10 11const axiosInstance = axios.create({ 12 baseURL: '/api' 13}); 14 15// 1. 请求拦截器:自动注入 Token 16axiosInstance.interceptors.request.use((config) => { 17 const accessToken = localStorage.getItem('access_token'); 18 if (accessToken && config.headers) { 19 config.headers.authorization = `Bearer ${accessToken}`; 20 } 21 return config; 22}); 23 24// 2. 响应拦截器:处理 Token 过期 25axiosInstance.interceptors.response.use( 26 (response) => response, 27 async (error) => { 28 const { data, config } = error.response; 29 30 // 情况 A:正在刷新 Token 中,将后续请求存入队列 31 if (refreshing) { 32 return new Promise((resolve) => { 33 queue.push({ config, resolve }); 34 }); 35 } 36 37 // 情况 B:access_token 过期 (状态码 401 且非刷新接口本身) 38 if (data.statusCode === 401 && !config.url.includes('/refresh')) { 39 refreshing = true; 40 41 try { 42 const res = await refreshToken(); 43 refreshing = false; 44 45 if (res.status === 200) { 46 // 核心逻辑:Token 刷新成功,回放队列中的所有请求 47 queue.forEach(({ config, resolve }) => { 48 resolve(axiosInstance(config)); 49 }); 50 queue = []; // 清空队列 51 52 // 执行当前触发刷新的那个请求 53 return axiosInstance(config); 54 } 55 } catch (err) { 56 refreshing = false; 57 queue = []; 58 // 情况 C:refresh_token 也过期了,彻底清除登录态 59 localStorage.clear(); 60 window.location.href = '/login'; 61 return Promise.reject(err); 62 } 63 } 64 65 return Promise.reject(error); 66 } 67); 68 69/** 70 * 刷新 Token 的异步方法 71 */ 72async function refreshToken() { 73 const res = await axios.get('/api/refresh', { 74 params: { 75 token: localStorage.getItem('refresh_token') 76 } 77 }); 78 // 更新本地存储 79 localStorage.setItem('access_token', res.data.accessToken); 80 localStorage.setItem('refresh_token', res.data.refreshToken); 81 return res; 82} 83
五、 注意事项
- 并发请求的 Promise 挂起:在
refreshing为true时,返回一个不带resolve的new Promise是关键,它能让 Axios 请求处于pending状态。 - 错误捕获:
refreshToken接口本身报错(如 500 或 401)必须妥善处理,直接引导至登录页。 - 安全性:普通项目中可以使用
localStorage,但在更高要求的项目中,建议配合HttpOnly Cookie存储refresh_token以防 XSS 攻击。 - 接口重定向陷阱:确保刷新 Token 的接口不会再次进入 401 拦截死循环。
《告别登录中断:前端双 Token无感刷新》 是转载文章,点击查看原文。