前端如何实现“无感刷新”Token?90% 的人都做错了

2026-04-28阅读 0热度 0
前端

今天,我们来彻底搞懂:如何真正实现“无感刷新”Token?为什么90%的实现都有致命缺陷?

在现代Web应用中,用户登录后通常会获得一对Token:Access Token(短期有效,如15分钟)和Refresh Token(长期有效,如7天)。

理想状态下,当Access Token过期时,前端应该自动用Refresh Token换取新Token,并悄无声息地重试原请求——整个过程用户毫无察觉,页面不跳转,操作不中断。

但现实往往很骨感,常见的场景是:“Token过期 → 弹出登录框 → 用户嘟囔一句‘怎么又登出了’ → 烦躁地关掉页面走人。”

这背后的体验鸿沟,正是我们今天要解决的核心问题。

1. 错误做法一:在每个接口里手动判断401

先看一个典型的反面教材:

// 千万别这么写!
fetch('/api/user')
  .then(res => {
    if (res.status === 401) {
      // 重新登录 or 刷新 token?
      window.location.href = '/login';
    }
  });

问题出在哪?

首先,每个接口都要重复编写这套判断逻辑,代码冗余且难以维护。其次,如果页面有多个请求同时返回401,会触发多次刷新甚至多次跳转登录页,逻辑混乱。最关键的是,这种方式完全背离了“无感”的初衷,用户体验极差。

2. 错误做法二:全局拦截401后直接刷新Token并重试一次

这是目前流传最广,但也最危险的“主流”方案:

// 伪代码:看似聪明,实则暗藏玄机
axios.interceptors.response.use(
  res => res,
  async (error) => {
    if (error.response.status === 401) {
      const newToken = await refreshToken(); // 获取新 token
      sa veToken(newToken);
      // 用新 token 重试原请求
      return axios(error.config);
    }
  }
);

表面上看逻辑通顺,但它至少隐藏了三个大坑:

(1) 坑1:并发请求雪崩

想象一下这个场景:页面刚加载,10个接口同时发起,而此时Token恰好过期。结果就是:10个请求全部返回401 → 触发10次独立的refreshToken()调用 → 后端瞬间收到10个刷新请求!

后果很严重:后端可能因安全策略拒绝重复刷新;Refresh Token被意外消耗,导致后续真正需要时失效;最坏情况下,用户反而被异常踢下线。

(2) 坑2:Refresh Token泄露风险

为了实现上述方案,前端通常需要读取并发送Refresh Token。如果将其存储在localStorage中,一旦遭遇XSS攻击,攻击者就能长期盗用该Token,账户安全形同虚设。

这里有一个关键的安全共识:Refresh Token应仅存于HttpOnly Cookie中,确保前端Ja vaScript无法直接读取!但上述方案要求前端“拿到新Token”,这就迫使开发者不得不将Refresh Token暴露给JS,陷入了安全与功能二选一的困境。

(3) 坑3:无限重试死循环

另一个可怕的陷阱是:如果refreshToken()接口本身也返回401(例如Refresh Token也已过期),那么代码逻辑会陷入死循环:尝试刷新 → 失败(401)→ 重试原请求 → 又触发401 → 再次尝试刷新……如此往复,浏览器可能卡死,内存占用飙升。

3. 正确方式:用“锁机制 + 队列 + 安全存储”三位一体

要实现真正健壮、安全、无感的刷新机制,必须同时解决三个核心问题:并发控制(确保只刷新一次)、安全存储(保护Refresh Token)、失败兜底(优雅处理刷新失败的情况)。

(1) 第一步:后端配合 —— Refresh Token存HttpOnly Cookie

安全基石由后端奠定。在设置Cookie时,务必加上HttpOnlySecure等安全标志:

HTTP/1.1 200 OK
Set-Cookie: refreshToken=abc123; HttpOnly; Secure; SameSite=Strict; Path=/auth

这样一来,前端永远无法通过Ja vaScript读取refreshToken,但浏览器在请求指定路径(如/auth)时会自动携带它,完美兼顾安全与功能。

(2) 第二步:前端实现“单例刷新锁 + 请求队列”

前端需要一套精密的拦截器逻辑来管理并发和状态。以下是核心实现思路:

let isRefreshing = false; // 刷新锁
let refreshPromise = null;
const failedQueue = []; // 重试队列

// 处理队列中的请求
const processQueue = (error, token = null) => {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) {
      reject(error);
    } else {
      resolve(token);
    }
  });
  failedQueue.length = 0; // 清空队列
};

axios.interceptors.response.use(
  response => response,
  async (error) => {
    const originalRequest = error.config;
    // 判断是否为401且未重试过
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // 已在刷新中,将当前请求加入队列,等待新token
        return new Promise((resolve, reject) => {
          failedQueue.push({ resolve, reject });
        }).then(token => {
          originalRequest.headers['Authorization'] = `Bearer ${token}`;
          return axios(originalRequest);
        });
      }

      // 标记开始刷新,防止并发
      originalRequest._retry = true;
      isRefreshing = true;

      try {
        // 调用刷新接口(后端从HttpOnly Cookie中读取refreshToken)
        const { data } = await axios.post('/auth/refresh');
        const newAccessToken = data.accessToken;

        // 刷新成功,通知所有在队列中等待的请求
        processQueue(null, newAccessToken);

        // 用新token重试当前触发刷新的请求
        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
        return axios(originalRequest);
      } catch (refreshError) {
        // 刷新失败:清除本地认证状态,跳转登录页
        clearAuth();
        processQueue(refreshError, null); // 通知队列中的所有请求失败
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        // 无论成功失败,最终都要释放锁
        isRefreshing = false;
        refreshPromise = null;
      }
    }
    return Promise.reject(error);
  }
);

这套设计的关键在于:用一个布尔锁(isRefreshing)控制刷新流程的单一性,用一个队列(failedQueue)收纳刷新期间失败的请求,待获取新Token后批量重试。 如此,便完美规避了并发雪崩和死循环问题。

4. 安全补充:前端Token存储建议

切记,切勿将任何Token存入localStorage 对于XSS攻击而言,localStorage就是敞开的保险柜。Access Token建议存储在内存或sessionStorage中(视会话需求而定),而Refresh Token,如前所述,应完全交由后端的HttpOnly Cookie管理。

5. 如何测试你的刷新逻辑?

理论需要实践检验。部署后,务必进行以下测试:

  1. 手动将当前Access Token设为过期状态。
  2. 在页面上快速点击多个按钮,触发并发API请求。
  3. 打开浏览器开发者工具的Network面板,观察:
    • 是否只发起了一次/auth/refresh调用?
    • 所有因401失败的原始请求,是否最终都成功返回了数据?
  4. 模拟Refresh Token失效(如清除对应Cookie),检查前端是否会正确跳转到登录页。

6. 结语

“无感刷新Token”并非炫技功能,而是对用户体验和系统安全的基本尊重。那些让用户频繁重新登录的产品,问题往往不在于技术做不到,而在于细节没有被认真对待。

真正的专业性,就藏在这些细节之中:一个简单的锁机制、一个高效的请求队列、一个安全的HttpOnly Cookie——这三者共同构成了那10%的正确方案与90%的错误实现之间的分水岭。

不妨审视一下,你的项目是否还在使用“遇到401就粗暴跳转登录”的方案?如果是,那么现在是时候升级了。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策