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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 14:42:03