普通网友 2026-02-27 14:15 采纳率: 98.5%
浏览 1
已采纳

UMI中requestInterceptors如何统一处理token过期并自动刷新?

在UMI项目中,使用`requestInterceptors`统一处理Token过期时,常遇到「刷新Token请求与原始请求并发触发、重复刷新、死循环重试」问题:当多个请求几乎同时发现401(Token过期),均触发`refreshToken`逻辑,导致多次调用刷新接口;若刷新失败或返回新Token未及时更新全局状态(如localStorage或model),后续请求仍携带旧Token而持续401;更严重的是,拦截器内直接`await refreshToken()`后重发原请求,若重发逻辑未加锁或去重,可能引发嵌套拦截、无限递归或竞态丢失。此外,UMI的`request`配置中`requestInterceptors`仅支持同步返回`options`,无法直接`await`异步刷新逻辑——这使得“自动续期+无缝重试”难以原生实现。如何在不侵入业务请求代码的前提下,基于UMI Request机制安全、幂等、可中断地完成Token刷新与请求重放?
  • 写回答

1条回答 默认 最新

  • 秋葵葵 2026-02-27 14:15
    关注
    ```html

    一、问题本质剖析:UMI Request 拦截器的同步性与 Token 刷新异步性的根本矛盾

    UMI 的 requestInterceptors 是纯同步钩子,仅允许返回修改后的 options,无法 await 异步操作(如 refreshToken)。当多个请求并发触发 401 时,每个拦截器实例均独立执行判断逻辑,缺乏跨请求状态共享机制——这导致「竞态刷新」成为必然而非偶然。核心矛盾在于:同步拦截器承载了本应由异步协调器完成的幂等续期任务

    二、典型错误模式复现(附代码反例)

    // ❌ 危险写法:无锁、无状态、无中断控制
    export const requestInterceptors = [
      (url, options) => {
        const token = getStoredToken();
        if (token) options.headers.Authorization = `Bearer ${token}`;
        return options;
      }
    ];
    
    // ❌ 错误响应拦截器:直接 await + 重发 → 触发嵌套拦截
    export const responseInterceptors = [
      async (response) => {
        if (response.status === 401) {
          await refreshToken(); // 多个请求同时执行 → 重复调用
          const newToken = getStoredToken();
          // ⚠️ 此处重发会再次进入 requestInterceptors → 无限递归风险!
          return request(response.config.url, { ...response.config, headers: { ...response.config.headers, Authorization: `Bearer ${newToken}` } });
        }
        return response;
      }
    ];

    三、高阶解决方案架构:三级协同防御体系

    层级职责关键技术点
    ① 请求门控层(Gatekeeper)全局唯一刷新任务调度器,阻塞并发请求,提供 Promise 缓存refreshQueue Map + Promise.allSettled 等待队列
    ② 状态管理层(State Orchestrator)统一维护 token 生命周期(valid/expired/refreshing)、原子更新 localStorage & modelZustand / Umi Model + useEffect 监听变更 + persist 插件
    ③ 拦截器适配层(Interceptor Bridge)将异步续期“桥接”进同步拦截流:通过占位符标记 + 延迟重试机制绕过同步限制options.__retryAfterRefresh = true + 全局 pending map

    四、关键实现:基于 Promise 缓存的幂等刷新控制器

    let refreshPromise: Promise<void> | null = null;
    
    export const ensureTokenValid = async (): Promise<boolean> => {
      const state = getTokenState(); // { valid: boolean, expiredAt: number }
      
      if (state.valid) return true;
      if (state.refreshing) {
        // ✅ 幂等:复用已有 Promise,避免重复发起刷新请求
        await refreshPromise;
        return getTokenState().valid;
      }
    
      // 🔒 原子标记为 refreshing,并创建唯一 Promise
      setTokenState({ ...state, refreshing: true });
      refreshPromise = (async () => {
        try {
          const { accessToken, refreshToken: newRT } = await api.refreshToken();
          persistTokens({ accessToken, refreshToken: newRT });
          setTokenState({ valid: true, refreshing: false, expiredAt: Date.now() + 3600000 });
        } catch (err) {
          clearAuthStorage();
          setTokenState({ valid: false, refreshing: false });
          throw err;
        }
      })();
    
      try {
        await refreshPromise;
        return true;
      } catch (err) {
        throw err;
      } finally {
        refreshPromise = null; // 释放引用,允许下次刷新
      }
    };

    五、UMI 拦截器桥接方案:延迟重试 + 请求暂存队列

    利用 UMI 的 request 配置中 errorConfig.adaptor 和自定义 request 封装,构建「拦截-暂存-恢复」闭环:

    • requestInterceptors 中检测 token 过期 → 注入 __deferred: true 标记
    • responseInterceptors 中捕获 401 → 不立即重发,而是加入 pendingRequests 队列
    • 调用 ensureTokenValid() → 成功后批量重放所有 pending 请求(带新 token)
    • 支持中断:任意 pending 请求可被 cancelToken 或超时主动丢弃

    六、流程图:Token 续期全链路状态机(Mermaid)

    graph TD A[请求发起] --> B{Token 是否有效?} B -- 是 --> C[正常发送] B -- 否 --> D[标记为 deferred 请求] D --> E[加入 pending 队列] E --> F[触发 ensureTokenValid] F --> G{刷新成功?} G -- 是 --> H[更新全局 token 状态] G -- 否 --> I[清空 auth,跳转登录] H --> J[批量重放 pending 请求] J --> K[移除 __deferred 标记,携带新 token 发送] K --> L[返回最终响应] I --> M[抛出 AUTH_REQUIRED 错误供业务层处理]

    七、生产级增强:可观察性与熔断保护

    为防刷新服务雪崩,需引入:

    • 指数退避重试:刷新失败后延迟 100ms/300ms/1s 递增重试,最多 3 次
    • 刷新熔断开关:连续 5 次刷新失败 → 自动开启 5 分钟熔断,期间所有请求直跳登录页
    • 调试埋点:通过 console.groupCollapsed 输出刷新耗时、并发请求数、重试次数
    • DevTools 集成:扩展 Umi 插件,在浏览器控制台暴露 window.$umiAuth.debug() 查看当前 token 状态

    八、不侵入业务的终极封装:自定义 Hook + 全局 Request 实例

    定义 useAuthenticatedRequest() Hook,内部自动注入 token 并接管 401 流程;同时导出一个已预配置拦截器的 authRequest 实例,业务侧只需:

    import { authRequest } from '@/utils/request';
    
    // ✅ 完全无感:自动处理续期、重试、错误降级
    const data = await authRequest.get('/api/user/profile');
    
    // 或搭配 SWR / RTK Query 使用
    const { data } = useSWR('/api/orders', authRequest.get);

    九、边界场景验证清单

    1. ✅ 页面打开瞬间 10 个并行请求全部 401 → 仅 1 次 refreshToken 调用
    2. ✅ 刷新接口超时 → 所有 pending 请求统一 reject,不重试
    3. ✅ 用户手动清除 localStorage → 立即触发登出,不尝试刷新
    4. ✅ 网络恢复后首个请求失败 → 自动重试且不丢失原始参数(query/body)
    5. ✅ React Strict Mode 双渲染 → 状态管理无副作用、Promise 不重复创建
    6. ✅ SSR 场景下 getServerSideProps 调用 → 跳过前端刷新逻辑,返回 401 让服务端处理

    十、演进思考:向 Request Middleware 范式迁移

    随着 Umi@4.3+ 对 requestMiddleware 的实验性支持,未来可将上述逻辑抽象为可插拔中间件:

    export const authMiddleware: RequestMiddleware = async (ctx, next) => {
      const { url, options } = ctx;
      if (isProtectedUrl(url)) {
        await ensureTokenValid();
        ctx.options = { ...options, headers: { ...options.headers, Authorization: `Bearer ${getStoredToken()}` } };
      }
      return next();
    };

    该范式天然支持 async/await、上下文透传、中间件组合,是解决此类问题的长期技术演进方向。

    ```
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

问题事件

  • 已采纳回答 2月28日
  • 创建了问题 2月27日