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

Apollo Server 4登录成功后context.user仍为空的问题求助

问题:Apollo Server 4登录后context.user始终为空

我正在使用Apollo Server 4开发,基于JWT和Cookie实现了登录系统,但登录成功后context.user始终为空,无法在请求间持久化。登录解析器已将JWT设置到Cookie中,浏览器网络面板能看到该Cookie,但后续请求的context中仍无用户信息。

我已做以下排查:

  • 实现了createContext函数,从Cookie提取并验证JWT,根据载荷设置context.user
  • 服务器已发送HttpOnly Cookie,浏览器可见,但后续请求似乎未携带该Cookie
  • 检查Apollo Server配置,确认context设置正确
  • 尝试不同浏览器及调整隐私设置,确保未拦截Cookie

相关代码

server.ts

import {ApolloServer} from '@apollo/server';
import {startStandaloneServer} from '@apollo/server/standalone';
import mongoose from 'mongoose';
import {typeDefs} from './application/graphql/typeDefs';
import {resolvers} from './application/graphql/resolvers';
import UserInterface from "./domain/interface/model/user.interface";
import jwt, {JwtPayload} from "jsonwebtoken";
import {IncomingMessage, ServerResponse} from "http";
import {User} from "./infrastructure/model/user.model";

mongoose.connect('mongodb://user-db:27017/user-service').then(() => {
    console.log('Connected to MongoDB');
});

require('./domain/config/passport');

interface ContextType {
    req: IncomingMessage;
    res: ServerResponse;
    user?: string | null;
}

const server = new ApolloServer<ContextType>({
    typeDefs,
    resolvers,
});

async function findUserById(id: string): Promise<UserInterface | null> {
    try {
        console.log("Find user by id:", id);
        return await User.findById(id);
    } catch (error) {
        console.error("Find user by id error:", error);
        return null;
    }
}

async function createContext({ req, res }: { req: IncomingMessage, res: ServerResponse }): Promise<ContextType> {
    let user: string | null = null;

    const token = req.headers.cookie?.split('; ')
        .find(cookie => cookie.startsWith('jwt='))
        ?.split('=')[1];

    if (token) {
        try {
            const decoded = jwt.verify(token, process.env.SECRET ?? 'SECRET_KEY');
            if (typeof decoded === 'object' && decoded.id) {
                const userData = await findUserById(decoded.id);
                user = userData ? userData.id : null;
                console.log("User:", user);
            }
        } catch (error) {
            console.error("JWT Error:", error);
            res.setHeader('Set-Cookie', 'jwt=; HttpOnly; Path=/; Max-Age=0');
        }
    }

    return { req, res, user };
}



startStandaloneServer(server, {
    listen: { port: 4001 },
    context: createContext
}).then(({ url }) => {
    console.log(`🚀 Server ready at ${url}`);
});

resolver.ts

import {User} from '../../infrastructure/model/user.model';
import bcrypt from 'bcryptjs';
import jwt, {JwtPayload} from 'jsonwebtoken';
import {
    loginValidationSchema,
    signUpValidationSchema,
    updateEmailValidationSchema,
    updateLocationValidationSchema,
    updatePasswordValidationSchema,
    updateValidationSchema
} from "../../infrastructure/dataValidation/mutation.validation";
import {
    ContextType,
    QueryArgs, UpdateLocation,
    UpdateUserInput,
    UserInput
} from "../../domain/interface/mutation/utils.mutation.interface";

function updateUserFields(userId: string, updateFields: object) {
    return User.findByIdAndUpdate(
        userId,
        {...updateFields, updated: new Date().toISOString()},
        {new: true}
    );
}

function verifyAuthenticatedUser(context: ContextType) {
    if (!context.user) throw new Error('You are not authenticated!');
    return context.user;
}

export const resolvers = {
    Query: {
        me: async (_: any, __: any, context: ContextType) => {
            const userId = verifyAuthenticatedUser(context);
            return User.findById(userId);
        },
    },
    Mutation: {
        signUp: async (_: any, args: { user: UserInput } , context: ContextType) => {
            const { error } = signUpValidationSchema.validate(args.user);
            if (error) throw new Error(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
            const newUser = new User({
                ...args.user,
                created: new Date().toISOString(),
                updated: new Date().toISOString(),
            });
            return await newUser.save();
        },
        login: async (_: any, {username, email, password}: QueryArgs, context: ContextType) => {
            const { error } = loginValidationSchema.validate({username, email, password});
            if (error) throw new Error(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
            if (!username && !email) throw new Error('Username or email is required!');
            const user = await User.findOne({$or: [{username}, {email}]});
            if (!user) throw new Error('Invalid credentials!');
            const isMatch = await bcrypt.compare(password, user.password);
            if (!isMatch) throw new Error('Invalid credentials!');

            const token = jwt.sign({id: user.id}, process.env.SECRET ?? 'SECRET_KEY', {expiresIn: '1h'});
            context.res.setHeader('Set-Cookie', `jwt=${token}; HttpOnly; Path=/; Max-Age=${60 * 60}`);
            context.user = user.id;
            return token;
        },
        logout: (_: any, __: any, context : ContextType) => {
            const userId = verifyAuthenticatedUser(context);
            const token = context.req.headers.cookie?.split('=')[1];
            if (!token || !userId) throw new Error('You are not authenticated!');
            try {
                const decoded = jwt.verify(token, process.env.SECRET ?? 'SECRET_KEY') as JwtPayload;
                if (decoded.id === userId) {
                    context.res.setHeader('Set-Cookie', 'jwt=; HttpOnly; Path=/; Max-Age=0');
                    return true;
                }
            } catch (error) {
                throw new Error('You are not authenticated!');
            }
        },
        updateUser: async (_: any, {updateFields}: { updateFields: UpdateUserInput }, context: ContextType) => {
            const { error } = updateValidationSchema.validate(updateFields);
            if (error) throw new Error(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
            const userId = verifyAuthenticatedUser(context);
            return await updateUserFields(userId, updateFields);
        },
        updatePassword: async (_: any, {password}: { password: string }, context: ContextType) => {
            const { error } = updatePasswordValidationSchema.validate({password});
            if (error) throw new Error(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
            const userId = verifyAuthenticatedUser(context);
            if (!password) throw new Error('Password cannot be empty!');
            const salt = await bcrypt.genSalt(10);
            const hashedPassword = await bcrypt.hash(password, salt);
            return await updateUserFields(userId, {password: hashedPassword});
        },
        updateLocation: async (_: any, {location}: { location: UpdateLocation }, context: ContextType) => {
            const { error } = updateLocationValidationSchema.validate({location});
            if (error) throw new Error(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
            const userId = verifyAuthenticatedUser(context);
            return await updateUserFields(userId, {location: location});
        },
        updateEmail: async (_: any, {email}: { email: string }, context: ContextType) => {
            const { error } = updateEmailValidationSchema.validate({email});
            if (error) throw new Error(`Validation error: ${error.details.map(x => x.message).join(', ')}`);
            const userId = verifyAuthenticatedUser(context);
            return await updateUserFields(userId, {email: email});
        },
        deleteUser: async (_: any, __: any, context: ContextType) => {
            const userId = verifyAuthenticatedUser(context);
            return await User.findByIdAndDelete(userId);
        },
    }
};

解决方案

1. 补充Cookie的SameSite属性

当前设置Cookie时未指定SameSite属性,现代浏览器默认按Lax规则处理,若客户端和服务端存在跨域(如客户端在localhost:3000,服务端在localhost:4001),Cookie可能无法被携带到后续请求。

修改登录和注销时的Cookie设置:

// 登录场景(本地开发用Lax,跨域生产环境用None+Secure)
context.res.setHeader('Set-Cookie', `jwt=${token}; HttpOnly; Path=/; Max-Age=${60 * 60}; SameSite=Lax`);

// 注销场景
context.res.setHeader('Set-Cookie', 'jwt=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax');

跨域场景必须使用SameSite=None; Secure,同时确保服务端开启HTTPS(本地开发可借助ngrok或自签证书)。

2. 确保客户端请求携带Cookie

如果使用Apollo Client调用接口,需在客户端配置中开启凭证携带:

const client = new ApolloClient({
  uri: 'http://localhost:4001',
  cache: new InMemoryCache(),
  credentials: 'include', // 关键配置
});

用GraphQL Playground测试时,需在设置中把"Request credentials"改为"include"。

3. 调试Cookie提取逻辑

在createContext函数中添加日志,确认请求是否携带Cookie:

async function createContext({ req, res }: { req: IncomingMessage, res: ServerResponse }): Promise<ContextType> {
    let user: string | null = null;

    console.log('Request Cookies:', req.headers.cookie); // 新增日志

    const token = req.headers.cookie?.split('; ')
        .find(cookie => cookie.startsWith('jwt='))
        ?.split('=')[1];

    // ... 剩余代码
}

若日志显示req.headers.cookie为空,说明浏览器未携带Cookie,回到前两步排查跨域和客户端配置。

4. 验证JWT密钥一致性

确保登录签名和createContext验证使用的密钥完全一致,检查process.env.SECRET是否正确加载,避免环境间密钥不一致导致验证失败。

5. 检查Cookie路径与域名

当前Path=/配置正确,但如果服务端使用子域名,需补充Domain属性(如Domain=.yourdomain.com),确保Cookie在整个域名下有效,本地开发一般无需设置。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 00:13:15