在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 }
问题分析与解决方案
当前代码的核心问题
- 类型不匹配:直接返回新的Access Token会违反GraphQL的类型定义——
getCartItems预期返回CartItemPayload,但实际返回字符串,必然导致客户端报错。 - 逻辑错误:
authenticate中间件里的else if ((isAuthenticated = "newAccess"))是赋值操作,不是判断,正确写法应为context.isAuthenticated === "newAccess"。 - 异步处理问题:
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', // 开启凭证携带 });
额外建议
- 用
httpOnlyCookie存储Token比存在Authorization头更安全,能避免XSS攻击。 - 服务端认证逻辑统一放在Context中处理,不要在Resolver中重复编写。
- 如果后续需要客户端处理Token刷新,可以用Apollo Link的错误拦截器:捕获Token过期错误后,自动调用Refresh Mutation更新Token,再重试原请求,但服务端处理的方案更简洁。
内容的提问来源于stack exchange,提问作者Caleb
相关产品推荐
相关产品推荐

