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

Express发送Set-Cookie但浏览器未设置的跨域问题排查

Express返回Set-Cookie响应头,但浏览器未存储该Cookie

问题场景

前端运行在localhost:7000,后端运行在localhost:4000。试过多种同类问题的配置方案均无效,Express已发送Set-Cookie响应头,但浏览器在Application→Cookies中找不到该Cookie。

技术栈

  • 前端:vite、react、@tanstack/react-query、graphql-request、@graphql-codegen、@graphql-codegen/typescript-react-query
  • 后端:@apollo/server、type-graphql、express、express-sessions

有复现仓库可用于排查问题。

代码片段

后端服务器代码

import 'reflect-metadata';
import 'dotenv/config';
import { expressMiddleware } from '@apollo/server/express4';
import http from 'http';
import { PrismaClient } from '@prisma/client';
import { ApolloServer } from '@apollo/server';
import express from 'express';
import bodyParser from 'body-parser';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import cors, { CorsRequest } from 'cors';
import session from 'express-session';
import buildSchemaFacade from './graphql/buildSchemaFacade';
import { redisStore } from './api/redis';

const SEVEN_DAYS = 1000 * 60 * 60 * 24 * 7;

const {
    NODE_ENV,
    PORT = 4000,
    SESSION_LIFETIME = SEVEN_DAYS,
    SESSION_SECRET
} = process.env;

async function bootstrap() {
    const prisma = new PrismaClient();

    const app = express();
    const httpServer = http.createServer(app);

    const schema = await buildSchemaFacade();

    const server = new ApolloServer({
        schema,
        plugins: [ApolloServerPluginDrainHttpServer({ httpServer })]
    });

    await server.start();

    app.use('/graphql', bodyParser.json());

    app.use(
        '/graphql',
        cors<CorsRequest>({
            origin: 'http://localhost:7000',
            credentials: true
        })
    );

    app.use(
        '/graphql',
        session({
            proxy: true,
            name: 'sessionID',
            cookie: {
                maxAge: SESSION_LIFETIME as number,
                sameSite: 'lax',
                secure: NODE_ENV === 'production',
                httpOnly: true
            },
            resave: false,
            secret: SESSION_SECRET as string,
            saveUninitialized: false,
            store: redisStore
        })
    );

    app.use(
        '/graphql',
        expressMiddleware(server, {
            context: async ({ req, res }) => ({ prisma, req, res })
        })
    );

    await new Promise<void>((resolve) =>
        httpServer.listen({ port: PORT || 4000 }, resolve)
    );

    console.log(`🚀 Server ready at http://localhost:4000/graphql`);
}

bootstrap();

前端GraphQL Client代码

import { GraphQLClient } from 'graphql-request';

export const graphqlClient = new GraphQLClient(import.meta.env.VITE_GRAPHQL_ENDPOINT as string, {
    headers: {
        credentials: 'include',
        mode: 'cors',
    },
});

Login.tsx代码

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { useLoginUserMutation } from '@platform/graphql/__generated__/graphql';
import { graphqlClient } from '@platform/graphql/graphqlClient';

export default function Login() {
    const [error, setError] = useState<string | null>(null);

    const { mutate } = useLoginUserMutation(graphqlClient);
    const navigate = useNavigate();

    const onSubmit = (event: any) => {
        event.preventDefault();

        const email = (event.target as HTMLFormElement).email.value;
        const password = (event.target as HTMLFormElement).password.value;

        mutate(
            { email, password },
            {
                onSuccess: (data) => {
                    // navigate('/dashboard/orders');
                    console.log(data);
                },
                onError: (error) => {
                    console.error(error);
                    setError('Something went wrong with logging in');
                },
            }
        );
    };

    return (
        <div className="flex h-screen items-center justify-center bg-slate-100">
            <div className="w-[300px] space-y-4  bg-white p-5">
                <h1 className="text-xl font-bold">Login</h1>
                <form onSubmit={onSubmit} className="flex flex-col space-y-4 text-lg">
                    <div className="flex flex-col space-y-2">
                        <label htmlFor="email">Email</label>
                        <input type="email" id="email" />
                    </div>
                    <div className="flex flex-col space-y-2">
                        <label htmlFor="password">Password</label>
                        <input type="password" id="password" />
                    </div>

                    <button type="submit" className="w-fit  bg-black p-2 px-4 text-white">
                        Login
                    </button>
                </form>
                {error && <div className="text-red-500">{error}</div>}
            </div>
        </div>
    );
}

./api/redis代码

import connectRedis from 'connect-redis';
import session from 'express-session';
import RedisClient from 'ioredis';

const RedisStore = connectRedis(session);
const redisClient = new RedisClient();

export const redisStore = new RedisStore({ client: redisClient });

export default redisClient;

登录请求响应截图

响应截图1
响应截图2
响应截图3
响应截图4

已尝试的解决方法

  • 将Cookie的secure字段设为true,sameSite设为none,并在GraphQL客户端添加x-forwarded-proto: https请求头,无效
  • 替换GraphQL客户端为urql和apollo client,问题依旧
  • 该功能在Insomnia中正常工作,但所有浏览器均不生效
  • 怀疑是后端express session初始化的问题,导致浏览器不接受Set-Cookie

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 23:30:45