告别频繁登录:Axios双Token刷新全攻略

告别频繁登录:教你用Axios实现无感知双Token刷新

一、问题背景:为何需要双Token刷新机制

在传统Web应用中,JWT(JSON Web Token)因其无状态特性被广泛用于身份认证。但单Token设计存在致命缺陷:当Token过期时,用户必须重新登录,尤其在SPA(单页应用)中体验极差。

典型痛点场景:

  1. 用户填写复杂表单时突然弹出登录框
  2. 视频播放中因Token过期中断
  3. 移动端网络不稳定时频繁触发401错误

双Token机制通过引入refreshToken(刷新令牌)完美解决这一问题:

  • accessToken:短期有效(如15分钟),用于访问API
  • refreshToken:长期有效(如7天),用于获取新accessToken

二、核心原理:Axios拦截器实现无感知刷新

1. Token存储与初始化

  1. // token存储示例(建议使用http-only cookie或加密localStorage)
  2. const TOKEN_KEY = {
  3. ACCESS: 'access_token',
  4. REFRESH: 'refresh_token'
  5. };
  6. // 初始化axios实例
  7. const api = axios.create({
  8. baseURL: 'https://api.example.com',
  9. timeout: 5000
  10. });

2. 请求拦截器:自动附加Token

  1. api.interceptors.request.use(
  2. config => {
  3. const accessToken = localStorage.getItem(TOKEN_KEY.ACCESS);
  4. if (accessToken) {
  5. config.headers.Authorization = `Bearer ${accessToken}`;
  6. }
  7. return config;
  8. },
  9. error => Promise.reject(error)
  10. );

3. 响应拦截器:处理Token过期

  1. let isRefreshing = false;
  2. let subscribers = [];
  3. api.interceptors.response.use(
  4. response => response,
  5. async error => {
  6. const { config, response } = error;
  7. // 401未授权错误处理
  8. if (response && response.status === 401) {
  9. // 防止重复刷新
  10. if (!isRefreshing) {
  11. isRefreshing = true;
  12. try {
  13. const refreshToken = localStorage.getItem(TOKEN_KEY.REFRESH);
  14. const { data } = await api.post('/auth/refresh', {
  15. refreshToken
  16. });
  17. // 更新存储的Token
  18. localStorage.setItem(TOKEN_KEY.ACCESS, data.accessToken);
  19. // 重试原始请求
  20. subscribers.forEach(cb => cb());
  21. subscribers = [];
  22. return api(config);
  23. } catch (refreshError) {
  24. // 刷新失败则跳转登录
  25. window.location.href = '/login';
  26. return Promise.reject(refreshError);
  27. } finally {
  28. isRefreshing = false;
  29. }
  30. }
  31. // 等待刷新时存储重试请求
  32. return new Promise(resolve => {
  33. subscribers.push(() => {
  34. config.headers.Authorization = `Bearer ${localStorage.getItem(TOKEN_KEY.ACCESS)}`;
  35. resolve(api(config));
  36. });
  37. });
  38. }
  39. return Promise.reject(error);
  40. }
  41. );

三、进阶实现:安全与优化策略

1. Token安全存储方案

存储方式 安全性 实现复杂度 适用场景
http-only Cookie ★★★★★ 传统Web应用
加密localStorage ★★★★ ★★★ SPA应用
Memory Storage 临时测试环境

2. 刷新Token失效处理

  1. // 增强版刷新逻辑
  2. async function refreshToken() {
  3. try {
  4. const refreshToken = getRefreshToken();
  5. if (!refreshToken) throw new Error('No refresh token');
  6. const { data } = await api.post('/auth/refresh', { refreshToken });
  7. // 验证新Token有效性
  8. const testResponse = await api.get('/auth/verify', {
  9. headers: { Authorization: `Bearer ${data.accessToken}` }
  10. });
  11. if (testResponse.status !== 200) {
  12. throw new Error('Invalid new access token');
  13. }
  14. return data.accessToken;
  15. } catch (error) {
  16. clearAllTokens();
  17. throw error;
  18. }
  19. }

3. 并发请求优化

  1. // 使用Promise.all处理并发刷新
  2. async function handleConcurrentRefresh(requests) {
  3. try {
  4. const newAccessToken = await refreshToken();
  5. return requests.map(req => {
  6. req.config.headers.Authorization = `Bearer ${newAccessToken}`;
  7. return api(req.config);
  8. });
  9. } catch (error) {
  10. return requests.map(() => Promise.reject(error));
  11. }
  12. }

四、最佳实践:完整实现示例

1. Token服务封装

  1. class TokenService {
  2. static getAccessToken() {
  3. return localStorage.getItem(TOKEN_KEY.ACCESS);
  4. }
  5. static setTokens({ accessToken, refreshToken }) {
  6. localStorage.setItem(TOKEN_KEY.ACCESS, accessToken);
  7. localStorage.setItem(TOKEN_KEY.REFRESH, refreshToken);
  8. }
  9. static clearTokens() {
  10. localStorage.removeItem(TOKEN_KEY.ACCESS);
  11. localStorage.removeItem(TOKEN_KEY.REFRESH);
  12. }
  13. static async refresh() {
  14. const refreshToken = this.getRefreshToken();
  15. if (!refreshToken) throw new Error('No refresh token');
  16. const { data } = await api.post('/auth/refresh', { refreshToken });
  17. this.setTokens(data);
  18. return data.accessToken;
  19. }
  20. }

2. 增强版Axios配置

  1. const enhancedApi = axios.create({
  2. // ...基础配置
  3. });
  4. let refreshLock = false;
  5. let retryQueue = [];
  6. enhancedApi.interceptors.response.use(
  7. response => response,
  8. async error => {
  9. const { config, response } = error;
  10. if (response?.status === 401) {
  11. if (!refreshLock) {
  12. refreshLock = true;
  13. try {
  14. const newAccessToken = await TokenService.refresh();
  15. // 重放所有排队请求
  16. retryQueue.forEach(cb => cb(newAccessToken));
  17. retryQueue = [];
  18. // 重试当前请求
  19. config.headers.Authorization = `Bearer ${newAccessToken}`;
  20. return enhancedApi(config);
  21. } catch (refreshError) {
  22. TokenService.clearTokens();
  23. window.location.href = '/login';
  24. return Promise.reject(refreshError);
  25. } finally {
  26. refreshLock = false;
  27. }
  28. }
  29. // 排队等待刷新
  30. return new Promise(resolve => {
  31. retryQueue.push(newAccessToken => {
  32. config.headers.Authorization = `Bearer ${newAccessToken}`;
  33. resolve(enhancedApi(config));
  34. });
  35. });
  36. }
  37. return Promise.reject(error);
  38. }
  39. );

五、部署注意事项

  1. CORS配置:确保刷新端点允许来自前端域的请求
  2. Token过期时间:建议accessToken设置15-30分钟,refreshToken设置7-30天
  3. CSRF防护:使用SameSite Cookie属性增强安全性
  4. 日志监控:记录Token刷新失败事件以便排查问题
  5. 移动端适配:考虑网络不稳定情况下的重试机制

六、效果对比

指标 单Token方案 双Token方案
用户中断次数 高(每次过期) 极低(无感知刷新)
开发复杂度 中等
安全性 中等 高(refreshToken可控)
适用场景 简单内部系统 用户型Web/移动应用

七、常见问题解答

Q1:refreshToken被盗用怎么办?
A:设置refreshToken的短有效期(如1天),结合设备指纹验证,发现异常立即失效所有Token。

Q2:如何处理多个标签页的Token刷新?
A:使用BroadcastChannel API或localStorage事件监听实现跨标签页同步。

Q3:微前端架构如何适配?
A:将Token服务封装为独立模块,通过全局事件或状态管理共享Token状态。

通过实施上述方案,您的Web应用将实现真正的无感知会话管理,用户再也不会因Token过期而被强制登录,大幅提升用户体验和系统稳定性。