You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ApolloClient实现refreshToken时出现无限循环问题求助

问题根因
  • 死循环核心原因:refreshTokens使用Apollo Client发起的查询请求会再次经过authMiddleware中间件,触发token过期校验,进而重复调用refreshTokens,形成自循环。
  • 刷新逻辑缺陷:你当前没有把refreshToken携带到刷新请求的请求头中,后端issueNewToken拿不到refreshToken会直接抛错,刷新始终失败,旧的过期token一直留在缓存中,每次校验都会触发刷新。
  • 状态未更新:就算刷新成功,你注释了accessTokenVar的更新代码,新token没有写入缓存,校验逻辑永远判断token已过期。
  • 无并发控制:如果同一时间有多个请求触发过期校验,会同时发起多个刷新请求,进一步加剧循环问题。
修复方案

1. 补充缓存变量

在cache.js中新增refreshToken的响应式存储变量:

import { makeVar } from "@apollo/client";

// 原有变量不变
export const refreshTokenVar = makeVar(null);

登录调用createOrGetUser成功后,需要把返回的token和refreshToken分别写入accessTokenVar和refreshTokenVar。

2. 改造App.js代码

首先补充必要导入:

import { Observable } from '@apollo/client/utilities';

添加刷新锁和请求队列逻辑,避免并发刷新:

let isRefreshing = false;
let pendingRequests = [];

// 刷新完成后批量执行排队的请求
const resolvePendingRequests = () => {
  pendingRequests.forEach(callback => callback());
  pendingRequests = [];
};

改造refreshTokens方法,刷新成功后更新缓存:

const refreshTokens = () => {
  return client.query({ 
    query: REFRESH_TOKENS,
    fetchPolicy: 'no-cache' // 禁用缓存,强制发起新请求
  }).then((response) => {
    const { token, refreshToken } = response.data.refreshTokens;
    accessTokenVar(token);
    refreshTokenVar(refreshToken);
    return { token, refreshToken };
  });
};

重写authMiddleware,添加刷新请求白名单、等待刷新逻辑:

const authMiddleware = new ApolloLink((operation, forward) => {
  let token = accessTokenVar();
  const operationName = operation.query.definitions[0].name?.value;
  
  // 刷新token的请求直接跳过过期检查,携带refreshToken到请求头
  if (operationName === 'refreshTokens') {
    const refreshToken = refreshTokenVar();
    operation.setContext(({ headers = {} }) => ({
      headers: {
        ...headers,
        refreshtoken: refreshToken || ""
      }
    }));
    return forward(operation);
  }

  if (token) {
    const pureToken = token.replace("Bearer ", "");
    const { exp } = jwtDecode(pureToken);
    const expirationTime = exp - 30;
    if (Date.now() / 1000 >= expirationTime) {
      if (!isRefreshing) {
        isRefreshing = true;
        refreshTokens()
          .then(() => resolvePendingRequests())
          .catch(() => {
            // 刷新失败清空登录状态,跳转到登录页
            accessTokenVar(null);
            refreshTokenVar(null);
            isLoggedInVar(false);
          })
          .finally(() => isRefreshing = false);
      }
      // 把当前请求加入队列,等刷新完成后再执行
      return new Observable(observer => {
        pendingRequests.push(() => {
          const newToken = accessTokenVar();
          operation.setContext(({ headers = {} }) => ({
            headers: {
              ...headers,
              authorization: newToken ? newToken.replace("Bearer ", "") : ""
            }
          }));
          forward(operation).subscribe(observer);
        });
      });
    }
  }

  operation.setContext(({ headers = {} }) => ({
    headers: {
      ...headers,
      authorization: token ? token.replace("Bearer ", "") : "",
    }
  }));

  return forward(operation);
});

3. 后端补充异常兜底

修改Auth辅助函数的issueNewToken方法,无refreshToken时主动抛错:

issueNewToken: async (req) => {
  try {
    const token = req.headers.refreshtoken;
    if (!token) {
      throw new AuthenticationError("Missing Refresh Token.");
    }
    // 原有逻辑保持不变
    const decoded = await jwt.verify(token, jwtRefreshTokenSecret);
    let user = await User.findById(decoded.id);
    if (!user) {
      throw new AuthenticationError("No user found.");
    }
    let tokens = await authFunctions.issueToken(user);
    return { ...tokens, user };
  } catch (err) {
    throw new AuthenticationError("Invalid Refresh Token.");
  }
},

内容的提问来源于stack exchange,提问作者Ruham

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 15:00:05