告别粗暴的401跳转:Token无感刷新完整解决方案

更新日期: 2026-04-02 阅读: 43 标签: token

刷新Token不是“过期就重新登录”,而是让用户毫无感知地继续使用。

可惜,大多数项目还在用401跳登录粗暴处理——这根本不是用户体验,这是放弃治疗。


一、为什么需要无感刷新

在现代Web应用中,用户登录后通常会获得一对Token:

  • Access Token(短期有效,如15分钟)

  • Refresh Token(长期有效,如7天)

当Access Token过期时,理想状态是:前端自动用Refresh Token换取新Token,并重试原请求——整个过程用户无感,页面不跳转、操作不中断。

但现实呢?

“Token过期 → 弹出登录框 → 用户骂一句‘怎么又登出了’ → 关掉页面走人。”

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


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

// ❌ 千万别这么写!
fetch('/api/user')
  .then(res => {
    if (res.status === 401) {
      window.location.href = '/login';
    }
  });

问题在哪?

  • 每个接口都要重复写逻辑

  • 如果多个请求同时401,会触发多次刷新,甚至多次跳登录

  • 完全无法做到“无感”



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

这是目前最“主流”的错误方案:

// ❌ 伪代码:看似聪明,实则危险
axios.interceptors.response.use(
  res => res,
  async (error) => {
    if (error.response.status === 401) {
      const newToken = await refreshToken();
      saveToken(newToken);
      return axios(error.config);
    }
  }
);

表面看没问题,但隐藏三大坑。

坑1:并发请求雪崩

当页面刚加载,10个接口同时发起,而此时Token已过期:

→ 10个请求全部返回401 → 触发10次refreshToken() → 后端收到10个刷新请求

后果:

  • 后端可能拒绝重复刷新(安全策略)

  • Refresh Token被提前消耗,后续真失效

  • 用户反而被踢下线

坑2:Refresh Token泄露风险

如果前端把Refresh Token存在localStorage,一旦XSS攻击成功,攻击者可长期盗用账号。

安全最佳实践:Refresh Token应仅存于HttpOnly Cookie,前端不可读。

但上述方案要求前端“拿到新token”,这就逼你把Refresh Token暴露给JS——安全与功能不可兼得。

坑3:无限重试死循环

如果refreshToken()本身也返回401(比如Refresh Token也过期了):

→ 重试原请求 → 又401 → 再刷新 → 再401 → …… 浏览器卡死,内存飙升。


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

要实现真正的无感刷新,必须同时解决:

  • 并发控制(只刷一次)

  • 安全存储(Refresh Token不暴露给JS)

  • 失败兜底(Refresh失败时优雅降级)

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

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

前端永远拿不到refreshToken,但每次请求会自动携带。

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

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;

    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 {
        // 调用刷新接口(后端从Cookie读refreshToken)
        const { data } = await axios.post('/auth/refresh');
        const newAccessToken = data.accessToken;

        // 通知所有排队的请求
        processQueue(null, newAccessToken);

        // 重试当前请求
        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队列缓存所有因401失败的请求,等新token到手后批量重试
_retry标记防止重试后的请求再次进入刷新逻辑
HttpOnly Cookie保护Refresh Token不被XSS窃取

五、安全补充:前端Token存储建议

Token类型推荐存储方式原因
Access Token内存(JS变量)或sessionStorage短期有效,避免持久化泄露
Refresh TokenHttpOnly Cookie前端不可读,防XSS

切勿将任何Token存入localStorage! 这是XSS攻击的黄金目标。


六、如何测试你的刷新逻辑

  1. 手动将Access Token设为过期

  2. 快速点击多个按钮,触发并发请求

  3. 观察Network面板:

    • 是否只调用了一次/auth/refresh?

    • 所有原请求是否最终成功?

  4. 模拟Refresh Token失效,是否跳转登录?


七、结语

“无感刷新Token”不是炫技,而是对用户体验和系统安全的基本尊重。

那些让用户频繁重新登录的产品,不是技术做不到,而是没把用户当回事。

真正的专业,藏在细节里:一个锁、一个队列、一个HttpOnly Cookie——这就是10%正确方案与90%错误实现的分水岭。

你的项目还在用“401就跳登录”吗?是时候升级了。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/13564

相关推荐

深度理解token

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库

koa+jwt实现token验证与刷新

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

前后端分离:使用 token 登录解决方案

这篇文章写一下前后端分离下的登录解决方案,目前大多数都采用请求头携带 Token 的形式。首次登录时,后端服务器判断用户账号密码正确之后,根据用户id、用户名、定义好的秘钥、过期时间生成 token

axios如何利用promise无痛刷新token?

前端登录后,后端返回token和token有效时间段tokenExprieIn,当token过期时间到了,前端需要主动用旧token去获取一个新的token,做到用户无感知地去刷新token。

深入理解令牌认证机制(token)

以前的开发模式是以MVC为主,但是随着互联网行业快速的发展逐渐的演变成了前后端分离,若项目中需要做登录的话,那么token成为前后端唯一的一个凭证。token即标志、记号的意思,在IT领域也叫作令牌。

vue+axios设置token,增加请求拦截器

第一次登录的时候,前端调后端的登陆接口,发送用户名和密码,后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token,前端拿到token,将token存储到localStorage和vuex中

axios如何利用promise无痛刷新token

最近遇到个需求:前端登录后,后端返回token和token有效时间,当token过期时要求用旧token去获取新的token,前端需要做到无痛刷新token,即请求刷新token时要做到用户无感知。

Vue 消除Token过期时刷新页面的重复提示

页面长时间未操作,再刷新页面时,第一次弹出“token失效,请重新登录!”提示,然后跳转到登录页面,接下来又弹出了n个“Token已过期”的后端返回消息提示。当前页面初始化,有多个向后端查询系统参数的调用,代码如下:

Token存储选择:LocalStorage、Cookie还是内存?几种方案对比

很多开发者刚开始做项目时都会遇到这个问题:用户登录后拿到的token,到底应该存在哪里?LocalStorage、SessionStorage、Cookie,还是直接放在内存中?为什么不同的网站做法不一样?

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!