❤ 写在前面
如果觉得对你有帮助的话,点个小❤❤ 吧,你的支持是对我最大的鼓励~
个人独立开发wx小程序,感谢支持! 
🎪 从游乐园门票说起
想象一下,你去游乐园玩,门票(Token)有一定有效期。传统方式中,门票过期时:
- 保安拦下你:"票过期了,去售票处重新买!"
- 你不得不离开项目,排队重新买票,再回来继续玩
而无感刷新就像有个贴心助手:
- 门票快过期时,助手悄悄帮你续期
- 你完全感知不到,继续畅玩各个项目
这就是我们今天要实现的用户体验!
🔍 为什么需要Token刷新?
Token的生命周期
markdown
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 登录获取 │────▶│ 使用Token │────▶│ Token过期 │
│ AccessToken │ │ 访问接口 │ │ 401错误 │
└─────────────┘ └─────────────┘ └─────────────┘
│
┌─────────────────────┘
▼
┌─────────────┐ ┌─────────────┐
│ 传统方式: │────▶│ 用户需重新 │
│ 跳转登录页 │ │ 登录 │
└─────────────┘ └─────────────┘
问题来了:每次Token过期都让用户重新登录,体验极差!
🎯 无感刷新的核心思路
graph TD
A[用户发起请求] --> B{Token是否有效?}
B -- 有效 --> C[正常请求]
B -- 已过期 --> D[拦截请求]
D --> E{是否正在刷新?}
E -- 否 --> F[发起刷新请求]
F --> G[获取新Token]
G --> H[重试原请求]
E -- 是 --> I[加入等待队列]
I --> J[刷新完成后重试]
C --> K[返回数据]
H --> K
J --> K
💻 实战代码实现(基于axios)
第一步:基础配置
javascript
// tokenManager.js
class TokenManager {
constructor() {
this.accessToken = localStorage.getItem('access_token');
this.refreshToken = localStorage.getItem('refresh_token');
this.isRefreshing = false; // 是否正在刷新
this.requestsQueue = []; // 请求等待队列
}
// 保存token
setTokens(accessToken, refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
}
// 清除token
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
}
第二步:axios拦截器设置
javascript
// http.js
import axios from 'axios';
import TokenManager from './tokenManager';
const tokenManager = new TokenManager();
const http = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 10000
});
// 请求拦截器
http.interceptors.request.use(
(config) => {
if (tokenManager.accessToken) {
config.headers.Authorization = `Bearer ${tokenManager.accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器 - 核心逻辑在这里!
http.interceptors.response.use(
(response) => {
// 正常响应直接返回
return response;
},
async (error) => {
const originalRequest = error.config;
// 如果不是401错误,直接返回
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
// 标记这个请求已经重试过,避免无限循环
originalRequest._retry = true;
// 如果没有refreshToken,跳转到登录页
if (!tokenManager.refreshToken) {
tokenManager.clearTokens();
window.location.href = '/login';
return Promise.reject(error);
}
// 如果正在刷新token,将请求加入队列
if (tokenManager.isRefreshing) {
return new Promise((resolve) => {
tokenManager.requestsQueue.push(() => {
originalRequest.headers.Authorization = `Bearer ${tokenManager.accessToken}`;
resolve(http(originalRequest));
});
});
}
// 开始刷新token
tokenManager.isRefreshing = true;
try {
// 调用刷新接口
const { data } = await axios.post('/api/auth/refresh', {
refresh_token: tokenManager.refreshToken
});
// 保存新token
tokenManager.setTokens(data.access_token, data.refresh_token);
// 执行等待队列中的所有请求
tokenManager.requestsQueue.forEach(callback => callback());
tokenManager.requestsQueue = [];
// 重试原始请求
originalRequest.headers.Authorization = `Bearer ${data.access_token}`;
return http(originalRequest);
} catch (refreshError) {
// 刷新失败,清除token并跳转登录
tokenManager.clearTokens();
tokenManager.requestsQueue = [];
window.location.href = '/login';
return Promise.reject(refreshError);
} finally {
tokenManager.isRefreshing = false;
}
}
);
export default http;
第三步:使用示例
javascript
// userService.js
import http from './http';
export const getUserInfo = async () => {
try {
const response = await http.get('/api/user/info');
return response.data;
} catch (error) {
console.error('获取用户信息失败:', error);
throw error;
}
};
export const updateProfile = async (data) => {
try {
const response = await http.post('/api/user/profile', data);
return response.data;
} catch (error) {
console.error('更新资料失败:', error);
throw error;
}
};
🎨 增强体验:添加视觉提示
虽然说是"无感",但适当的提示能让体验更好:
javascript
// 在刷新token时显示加载提示
let refreshLoading = null;
// 修改响应拦截器中的刷新部分
try {
// 显示轻量级提示
refreshLoading = showLoading('正在更新登录状态...');
const { data } = await axios.post('/api/auth/refresh', {
refresh_token: tokenManager.refreshToken
});
// 隐藏提示
refreshLoading?.hide();
showToast('登录状态已更新', 'success', 2000);
// ... 其余逻辑
} catch (error) {
refreshLoading?.hide();
showToast('登录已过期,请重新登录', 'error');
// ... 其余错误处理
}
🛡️ 安全注意事项
-
Refresh Token有效期:通常比Access Token长,但也不是永久的
-
单次使用:每次使用Refresh Token后,服务端应该颁发新的Refresh Token
-
安全存储 :
javascript// 使用更安全的方式存储 const secureStorage = { setItem: (key, value) => { if (window.crypto && window.crypto.subtle) { // 考虑使用加密存储 localStorage.setItem(key, value); } else { // 降级方案 localStorage.setItem(key, value); } }, getItem: (key) => localStorage.getItem(key) };
🎪 回到游乐园比喻
现在我们的系统就像这样工作:
markdown
游乐园项目(API请求) → 检票口(拦截器)
│
├── 票有效 → 直接进入
│
├── 票过期,有续票资格 → 助手悄悄续票 → 继续游玩
│
└── 票过期,无续票资格 → 引导重新购票(登录)
📊 性能优化小贴士
javascript
// 1. 预刷新:在token即将过期时提前刷新
const shouldRefreshToken = () => {
const tokenExpiry = getTokenExpiry(tokenManager.accessToken);
const now = Date.now();
// 在过期前5分钟开始刷新
return tokenExpiry - now < 5 * 60 * 1000;
};
// 2. 定时检查
setInterval(() => {
if (shouldRefreshToken() && !tokenManager.isRefreshing) {
refreshTokenSilently();
}
}, 60000); // 每分钟检查一次
// 3. 并发控制优化
const MAX_QUEUE_SIZE = 50;
if (tokenManager.requestsQueue.length > MAX_QUEUE_SIZE) {
// 队列过长,可能是异常情况
tokenManager.requestsQueue = [];
window.location.reload(); // 或采取其他恢复措施
}
🎉 总结
实现Token无感刷新的关键在于:
- 拦截401错误:在axios响应拦截器中捕获
- 避免并发刷新:用标志位和队列控制
- 优雅降级:刷新失败时友好引导重新登录
- 用户体验:适当的提示(但不是打断)
现在你的应用就像那个贴心的游乐园助手,让用户在不知不觉中保持登录状态,享受流畅的体验!
试试实现它,让你的应用告别烦人的"登录已过期"提示吧!🚀
小作业:你能想到在哪些场景下,即使实现了无感刷新,仍然需要主动提示用户重新登录吗?欢迎在评论区分享你的想法!💭