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

在Apollo中实现Access Token与Refresh Token,拦截请求返回新Token避免报错

问题:Access Token过期时服务端返回新Token而非原查询结果

我想拦截Apollo Client发往Apollo Server的请求,当用户Access Token过期时,通过验证Refresh Token返回新的Access Token,而不是原查询预期的CartItemPayload数据,同时保证客户端不触发错误。

我原本考虑在客户端新增refreshToken mutation,每次请求前先解码Access Token判断是否过期再发起对应请求,但觉得这个问题应该在服务端解决。刚接触Apollo和GraphQL,没使用Express中间件及Redux客户端中间件,不确定当前写法是否可行,求解决方案或建议。


当前代码

Server

const server = new ApolloServer({
  typeDefs,
  resolvers,
  cors: {
    origin: "http://localhost:3000",
    credentials: true,
  },
  context: async ({ req, res }) => {
    let isAuthenticated = false;
    // get the user token from the headers
    const authorization = req.get("Authorization");
    if (authorization) {
      let token = req.headers.authorization.replace(/\(/g, "");
      let verifysomething = jwt.verify(
        token,
        process.env.JWT_SECRET,
        (err, decoded) => {
          console.log("access decoded");
          console.log(decoded);
          if (err) {
            return err;
          }

          return decoded;
        }
      );
      if (verifysomething.message == "jwt expired") {
        let refreshToken = req.headers.cookie.replace(/^[^=]+=/, "");
        let verifyRefresh = jwt.verify(
          refreshToken,
          process.env.JWT_SECRET,
          (err, decoded) => {
            if (err) return err;

            const accessToken = jwt.sign(
              { userId: decoded.userId },
              process.env.JWT_SECRET,
              {
                expiresIn: "20s",
              }
            );
            isAuthenticated = "newAccess";
            return accessToken;
          }
        );
         //end return if now access token but has refresh token
        return { isAuthenticated, res, verifyRefresh };
      }

      let user = await User.findOne({ _id: verifysomething.userId });
      if (user) {
        isAuthenticated = true;

        return {
          isAuthenticated,
          user,
        };
      }
    }

    return { isAuthenticated, res };
  },
});

Resolvers

const resolvers = {
  Query: {
    getCartItems: authenticate(
      async (_, { id }) =>
        await cartItem.find({ user: id }).populate("user").populate("item")
    ),
  }}

Authenticate

function authenticate(resolver) {
  return function (root, args, context, info) {
    if (context.isAuthenticated == true) {
      return resolver(root, args, context, info);
    } 
    else if ((isAuthenticated = "newAccess")) {
      return context.verifyRefresh;
    }
    throw new Error(`Access Denied!`);
  };
}

预期返回类型

type CartItemPayload {
  item: Item
  user: User
}

问题分析与解决方案

当前代码的核心问题

  1. 类型不匹配:直接返回新的Access Token会违反GraphQL的类型定义——getCartItems预期返回CartItemPayload,但实际返回字符串,必然导致客户端报错。
  2. 逻辑错误:authenticate中间件里的else if ((isAuthenticated = "newAccess"))是赋值操作,不是判断,正确写法应为context.isAuthenticated === "newAccess"。
  3. 异步处理问题:jwt.verify用回调写法时,同步获取结果会导致verifysomething不是预期的解码结果/错误对象,逻辑会混乱。

正确的服务端处理思路

不要在查询中返回Token,而是通过Set-Cookie响应头把新Token发给客户端,同时继续执行原查询,返回预期的CartItemPayload,这样客户端既拿到了业务数据,又自动更新了Token,不会触发错误。

修改后的代码示例

1. 重构Context中的Token验证逻辑

const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
// 必须先挂载cookie-parser才能解析req.cookies
app.use(cookieParser());

const server = new ApolloServer({
  typeDefs,
  resolvers,
  cors: {
    origin: "http://localhost:3000",
    credentials: true,
  },
  context: async ({ req, res }) => {
    let isAuthenticated = false;
    let user = null;
    const authorization = req.get("Authorization");
    
    if (authorization) {
      const token = authorization.replace(/\(/g, "");
      try {
        // 同步验证Access Token,用try/catch捕获过期/无效错误
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        user = await User.findOne({ _id: decoded.userId });
        isAuthenticated = !!user;
      } catch (err) {
        // 仅处理Access Token过期的情况
        if (err.name === "TokenExpiredError") {
          // 从cookie中提取Refresh Token(建议明确指定cookie key,而非正则截取)
          const refreshToken = req.cookies?.refreshToken;
          if (refreshToken) {
            try {
              const decodedRefresh = jwt.verify(refreshToken, process.env.JWT_SECRET);
              // 生成新的Access Token
              const newAccessToken = jwt.sign(
                { userId: decodedRefresh.userId },
                process.env.JWT_SECRET,
                { expiresIn: "20s" }
              );
              // 通过Set-Cookie返回新Token,配置安全属性
              res.cookie("accessToken", newAccessToken, {
                httpOnly: true,
                secure: process.env.NODE_ENV === "production",
                sameSite: "strict",
                maxAge: 20 * 1000 // 和Token过期时间一致
              });
              // 重新获取用户信息,继续原查询
              user = await User.findOne({ _id: decodedRefresh.userId });
              isAuthenticated = !!user;
            } catch (refreshErr) {
              // Refresh Token无效,认证失败
              isAuthenticated = false;
            }
          }
        }
      }
    }

    return { isAuthenticated, user, res };
  },
});

// 挂载Apollo Server到Express
await server.start();
server.applyMiddleware({ app, cors: false }); // 已在Apollo配置cors,此处关闭

2. 简化Authenticate中间件

function authenticate(resolver) {
  return function (root, args, context, info) {
    if (context.isAuthenticated) {
      return resolver(root, args, context, info);
    }
    throw new Error(`Access Denied!`);
  };
}

3. 客户端配置补充

确保Apollo Client允许携带凭证,才能正确接收和发送Cookie:

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
  credentials: 'include', // 开启凭证携带
});

额外建议

  • 用httpOnly Cookie存储Token比存在Authorization头更安全,能避免XSS攻击。
  • 服务端认证逻辑统一放在Context中处理,不要在Resolver中重复编写。
  • 如果后续需要客户端处理Token刷新,可以用Apollo Link的错误拦截器:捕获Token过期错误后,自动调用Refresh Mutation更新Token,再重试原请求,但服务端处理的方案更简洁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 01:10:27