如何在Next.js中间件中通过GraphQL查询获取已认证用户
我的Next.js 14应用与Apollo Server GraphQL后端分属不同服务器,后端使用graphql-passport存储会话与已认证用户。登录时Passport会设置connect.sid Cookie,并在MongoDB中创建会话条目。我有一个authUser的GraphQL查询,在Apollo Provider包裹的组件中可以正常获取已认证用户,但无法在Next.js的middleware.ts中使用该查询验证用户身份、处理受保护路由逻辑。我希望通过该查询从后端验证用户(而非仅检查Cookie),尝试过Server actions、client.query、UseQuery等方法均无效,不想使用Prisma或Server actions,需保持前后端分离,寻求基于现有技术栈的解决方案。
相关代码文件
User.resolver.js(后端)
import bcrypt from "bcryptjs"; import User from "../models/User.Schema.js"; const userResolver = { Mutation: { signUp: async (_, { input }, context) => { try { const { email, password, name } = input; if (!email || !password || !name) { throw new Error("All fields are required"); } const existingUser = await User.findOne({ email }); if (existingUser) { throw new Error("User already exists"); } const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); let newUser; newUser = new User({ email, password: hashedPassword, name, }); await newUser.save(); await context.login(newUser); return newUser; } catch (error) { console.log("Error in signup: ", error); throw new Error(error.message || "Internal Server Error"); } }, signUpAdmin: async (_, { input }, context) => { try { const { email, password, name, orgaizationName } = input; } catch (error) {} }, login: async (_, { input }, context) => { try { const { email, password } = input; if (!email || !password) { throw new Error("All fields are required"); } const { user } = await context.authenticate("graphql-local", { email, password, }); await context.login(user); return user; } catch (error) { console.log("Error in login: ", error); if (error.name === "AuthenticationError") { throw new Error("Incorrect username or password"); } else { throw new Error(error.message || "Internal server error"); } } }, }, Query: { authUser: async (_, __, context) => { try { console.log("CONTEXT: ", context); const user = await context.req.user; console.log(user); return user; } catch (error) { console.log("Error in authUser: ", error); throw new Error(error.message || "Internal Server Error"); } }, }, }; export default userResolver;
index.ts(后端)
import express from "express"; import http from "http"; import cors from "cors"; import path, { resolve } from "path"; import { ApolloServer } from "@apollo/server"; import { expressMiddleware } from "@apollo/server/express4"; import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer"; import dotenv from "dotenv"; import passport from "passport"; import { buildContext } from "graphql-passport"; import session from "express-session"; import connectMongo from "connect-mongodb-session"; import { configurePassport } from "./passport/passport.config.js"; import { connectDb } from "./utils/connectDb.js"; import mergedTypeDefs from "./typeDefs/index.js"; import mergedResolvers from "./resolvers/index.js"; dotenv.config(); configurePassport(); const __dirname = path.resolve(); const app = express(); const httpServer = http.createServer(app); const MongoDBStore = connectMongo(session); const store = new MongoDBStore({ uri: process.env.MONGO_URI, collection: "sessions", }); store.on("error", (error) => { console.log(error); }); app.use( session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, store: store, cookie: { maxAge: 1000 * 60 * 60 * 24 * 7, // 1 week httpOnly: true, }, }) ); app.use(passport.initialize()); app.use(passport.session()); const server = new ApolloServer({ typeDefs: mergedTypeDefs, resolvers: mergedResolvers, plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); await server.start(); app.use( "/graphql", cors({ origin: "http://localhost:3000", credentials: true }), express.json(), expressMiddleware(server, { context: async ({ req, res }) => buildContext({ req, res }), }) ); app.use(express.static(path.join(__dirname, "../client/build"))); await new Promise((resolve) => httpServer.listen({ port: 8080 }, resolve)); await connectDb(); console.log(`Server ready at: http://localhost:8080/graphql`);
auth.ts(Next.js应用)
import { client } from "@/components/apolloProvider/provider"; import { gql } from "@apollo/client"; export async function isUserAuthenticated(req) { try { const { data } = await client.query({ query: gql` query GetAuthenticatedUser { authUser { uniqueId } } `, }); console.log("data: ", data); return !!data.authenticatedUser; } catch (error) { console.error("Error checking authentication:", error); return false; } }
middleware.ts(Next.js应用)
import { isUserAuthenticated } from "./auth"; import { DEFAULT_LOGIN_REDIRECT, apiAuthPrefix, authRoutes, publicRoutes, } from "@/routes"; import { cookies } from "next/headers"; import { gql } from "@apollo/client"; import { client } from "./components/apolloProvider/provider"; export async function middleware(req) { const { nextUrl } = req; const isLoggedIn = await isUserAuthenticated(req); const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix); const isPublicRoute = publicRoutes.includes(nextUrl.pathname); const isAuthRoute = authRoutes.includes(nextUrl.pathname); if (isApiAuthRoute) { return null; } if (isAuthRoute) { if (isLoggedIn) { return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl)); } return null; } if (!isLoggedIn && !isPublicRoute) { return Response.redirect(new URL("/sign-in", nextUrl)); } return null; } export const config = { matcher: [ "/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)", ], };
解决方案
核心问题分析
Next.js的Middleware运行在边缘运行时,你当前使用的Apollo Client是为客户端/Node.js环境设计的,直接在Middleware中调用client.query会因Cookie传递不正确、运行时环境不兼容导致失败。另外现有isUserAuthenticated函数未将请求中的connect.sid Cookie传递给后端,导致后端无法识别会话。
1. 修改auth.ts:手动构造GraphQL请求并传递会话Cookie
放弃Apollo Client,改用原生fetch向GraphQL后端发送请求,同时传递请求中的connect.sid Cookie,让后端能验证会话有效性:
export async function isUserAuthenticated(req) { // 从请求头中获取Cookie const cookieHeader = req.headers.get('cookie'); if (!cookieHeader) return false; try { const response = await fetch('http://localhost:8080/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cookie': cookieHeader, // 传递会话Cookie给后端 }, body: JSON.stringify({ query: ` query GetAuthenticatedUser { authUser { uniqueId } } ` }), credentials: 'include', }); if (!response.ok) throw new Error('请求后端失败'); const { data, errors } = await response.json(); if (errors) throw new Error(errors[0].message); // 验证authUser是否存在 return !!data?.authUser; } catch (error) { console.error('验证用户身份失败:', error); return false; } }
2. 后端跨域Cookie配置(跨域环境必填)
如果前后端为完全跨域(不同域名),修改后端index.ts中的session配置,确保Cookie支持跨域:
app.use( session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, store: store, cookie: { maxAge: 1000 * 60 * 60 * 24 * 7, httpOnly: true, sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax', secure: process.env.NODE_ENV === 'production', // 生产环境需开启HTTPS }, }) );
同时更新CORS配置,使用环境变量管理前端地址:
app.use( "/graphql", cors({ origin: process.env.FRONTEND_URL || "http://localhost:3000", credentials: true }), express.json(), expressMiddleware(server, { context: async ({ req, res }) => buildContext({ req, res }), }) );
3. 保留原有Middleware逻辑
你的middleware.ts路由判断逻辑无需修改,只需确保isUserAuthenticated函数替换为上述实现即可。
内容的提问来源于stack exchange,提问作者Pranav Bhat

