Token已过期,我是如何实现无感刷新Token的?

Token已过期,我是如何实现无感刷新Token的?

你有没有遇到过这样的场景:正在电商网站上精心挑选商品,填好了复杂的收货地址,满心欢喜地点击提交订单按钮,结果页面突然跳转到登录页,提示"登录状态已过期,请重新登录"?

那一刻,你的内心是什么感受?我想大概率是崩溃的,并且想把这个网站拉进黑名单。

这就是一个典型的、因为Token过期处理不当而导致的灾难级用户体验。作为一个负责任的开发者,这是我们绝对不能接受的。

今天就聊聊,我们团队是如何通过请求拦截和队列控制,来实现无感刷新Token的。让用户即使在Token过期的情况下,也能无缝地继续操作,就好像什么都没发生过一样。

一、为什么需要无感刷新Token?

在现代Web应用中,为了安全考虑,我们通常会使用JWT(JSON Web Token)进行用户身份验证。但JWT有一个特点:有过期时间。

如果Token过期了,用户就需要重新登录,这会带来以下问题:

  1. 糟糕的用户体验:用户正在操作中突然被要求重新登录
  2. 数据丢失风险:用户未保存的数据可能丢失
  3. 操作中断:正在进行的业务流程被迫中断

无感刷新Token就是为了解决这些问题,让用户在不知情的情况下自动续期Token,保证业务流程的连续性。

二、双Token机制原理

要实现无感刷新,我们需要后端同学的配合,采用双Token的认证机制:

2.1 两种Token的区别

  • accessToken:这是我们每次请求业务接口时,都需要在请求头里带上的令牌。它的特点是生命周期短(比如1小时),因为暴露的风险更高。
  • refreshToken:它的唯一作用,就是用来获取一个新的accessToken。它的特点是生命周期长(比如7天),并且需要被安全地存储(比如HttpOnly的Cookie里)。

2.2 工作流程

  1. 用户登录成功后,后端会同时返回accessToken和refreshToken
  2. 前端将accessToken存在内存(或LocalStorage)里,然后在后续的请求中使用
  3. 当accessToken过期时,使用refreshToken来刷新获取新的accessToken
  4. 拿到新的accessToken后,重新执行之前失败的请求

三、核心实现思路

我们整个方案的核心,是利用axios(或其他HTTP请求库)提供的请求拦截器(Interceptor)。它就像一个哨兵,可以在请求发送前和响应返回后,对请求进行拦截和改造。

我们的目标是:

  1. 在响应拦截器里,捕获到后端返回的accessToken已过期的错误(通常是401状态码)
  2. 当捕获到这个错误时,暂停所有后续的API请求
  3. 使用refreshToken,悄悄地在后台发起一个获取新accessToken的请求
  4. 拿到新的accessToken后,更新我们本地存储的Token
  5. 最后,把之前失败的请求和被暂停的请求,用新的Token重新发送出去

这个过程对用户来说,是完全透明的。他们最多只会感觉到某一次API请求,比平时慢了一点点。

四、具体实现代码

下面是我们团队在项目中实际使用的axios拦截器实现代码:

import axios from 'axios';

// 创建一个新的axios实例
const api = axios.create({
  baseURL: '/api',
  timeout: 5000,
});

// ------------------- 请求拦截器 -------------------
api.interceptors.request.use(config => {
  const accessToken = localStorage.getItem('accessToken');
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
}, error => {
  return Promise.reject(error);
});

// ------------------- 响应拦截器 -------------------

// 用于标记是否正在刷新token
let isRefreshing = false;
// 用于存储因为token过期而被挂起的请求
let requestsQueue = [];

api.interceptors.response.use(
  response => {
    return response;
  },
  async error => {
    const { config, response } = error;

    // 如果返回的HTTP状态码是401,说明access_token过期了
    if (response && response.status === 401) {
      // 如果当前没有在刷新token,那么我们就去刷新token
      if (!isRefreshing) {
        isRefreshing = true;

        try {
          // 调用刷新token的接口
          const { data } = await axios.post('/refresh-token', {
            refreshToken: localStorage.getItem('refreshToken')
          });

          const newAccessToken = data.accessToken;
          localStorage.setItem('accessToken', newAccessToken);

          // token刷新成功后,重新执行所有被挂起的请求
          requestsQueue.forEach(cb => cb(newAccessToken));
          // 清空队列
          requestsQueue = [];

          // 把本次失败的请求也重新执行一次
          config.headers.Authorization = `Bearer ${newAccessToken}`;
          return api(config);

        } catch (refreshError) {
          // 如果刷新token也失败了,说明refreshToken也过期了
          // 此时只能清空本地存储,跳转到登录页
          console.error('Refresh token failed:', refreshError);
          localStorage.removeItem('accessToken');
          localStorage.removeItem('refreshToken');
          // window.location.href = '/login';
          return Promise.reject(refreshError);
        } finally {
          isRefreshing = false;
        }
      } else {
        // 如果当前正在刷新token,就把这次失败的请求,存储到队列里
        // 返回一个pending的Promise,等token刷新后再去执行
        return new Promise((resolve) => {
          requestsQueue.push((newAccessToken) => {
            config.headers.Authorization = `Bearer ${newAccessToken}`;
            resolve(api(config));
          });
        });
      }
    }

    return Promise.reject(error);
  }
);

export default api;

五、关键点解析

5.1 isRefreshing 状态锁

这是为了解决并发问题。想象一下,如果一个页面同时发起了3个API请求,而accessToken刚好过期,这3个请求会同时收到401。如果没有isRefreshing这个锁,它们会同时去调用/refresh-token接口,发起3次刷新请求,这是完全没有必要的浪费,甚至可能因为并发问题导致后端逻辑出错。

有了这个锁,只有第一个收到401的请求,会真正去执行刷新逻辑。

5.2 requestsQueue 请求队列

当第一个请求正在刷新Token时(isRefreshing = true),后面那2个收到401的请求怎么办?我们不能直接抛弃它们。正确的做法,是把它们的resolve函数推进一个队列(requestsQueue)里,暂时挂起。

等第一个请求成功拿到新的accessToken后,再遍历这个队列,把所有被挂起的请求,用新的Token重新执行一遍。

六、安全注意事项

在实现无感刷新Token时,还需要注意以下安全问题:

  1. refreshToken存储安全:建议存储在HttpOnly Cookie中,防止XSS攻击
  2. refreshToken有效期:不宜设置过长,避免安全风险
  3. 刷新接口限流:防止恶意刷接口
  4. 异常处理:refreshToken失效时要及时清理本地存储并跳转登录页

七、总结

无感刷新Token这个功能,用户成功的时候,是感知不到它的存在的。

但恰恰是这种无感的细节,区分出了一个能用的应用和一个好用的应用。

因为一个资深的开发者,他不仅关心功能的实现,更应该关心用户体验和整个系统的健壮性。

希望这一套解决思路,能对你有所帮助🤞😁。

在实际项目中,你可能还需要根据具体的业务场景进行调整,比如添加重试次数限制、更完善的错误处理等。但核心的思路和实现方式都是相通的。

掌握了这个技术,你就能让用户在使用你的产品时,再也不用担心因为Token过期而被迫中断操作了!


标题:Token已过期,我是如何实现无感刷新Token的?
作者:jiangyi
地址:http://jiangyi.space/articles/2025/12/21/1766304286316.html

    0 评论
avatar