NodeJS Graphql Set-正在发送但未设置Cookie

quhf5bfb  于 2023-02-12  发布在  Node.js
关注(0)|答案(3)|浏览(123)

在尝试了不同的配置后发现类似的问题,他们似乎都没有工作。设置Cookie是由快递发送,但浏览器没有设置它在应用程序-〉Cookie
此问题适用于我在localhost:7000上运行前端而在localhost:4000上运行后端的情况
前端技术:vite,react,@tansack/react-查询,图形请求(提取器),@图形代码生成器,@图形代码生成器/typescript-react-查询(使用此函数为图形生成React查询钩子)
后端技术:@apollo/服务器,类型-图形ql,快速,快速会话
再生产回购:https://github.com/Waqas-Abbasi/cookies-not-set-repro
后端服务器:

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客户端:

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>
    );
}

按下登录按钮时的响应:
第一节第一节第一节第一节第二节第一节第三节第一节
编辑:
我也尝试了解决方案:
https://community.apollographql.com/t/cookie-not-shown-stored-in-the-browser/1901/8
将cookie的安全字段设置为true,将sameSite设置为none,并在graphQL客户端中传递值为httpsx-forwarded-proto
尽管如此,它仍然不起作用。顺便说一句,它在失眠症上的工作和预期的一样,只是在任何浏览器上都不起作用
编辑2:我也尝试过用urql和apolloclient替换graphql-request,还是同样的问题,这让我想到这可能是后端的问题,比如express会话是如何初始化的,而且由于某种原因,浏览器不喜欢从后端发送的Set-Cookie
编辑3:
./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;

编辑四:
再生产回购:https://github.com/Waqas-Abbasi/cookies-not-set-repro

yacmzcpb

yacmzcpb1#

在GraphQL客户端定义中,将

headers: {
  credentials: 'include',
  mode: 'cors',
}

credentials: 'include',
mode: 'cors'

因为它们不是报头,所以它们是请求的参数。
如果请求中没有credentials: 'include'参数,fetch方法将既不会发送也不会接收任何 cross-origin cookie,其中端口的差异(localhost:7000 vs. localhost:4000)已经使请求跨源。
Wesley LeMahieu的答案涉及到当一个cookie被算作 same-site 时的问题,其中端口在获取一个站点时并不重要。localhost127.0.0.1不是same-site。
只有当fetch请求是同一站点时,才会发送或接收带有SameSite: Lax的Cookie。在问题中,fetch请求是同一站点,但跨源,因此需要credentials: 'include'选项。

frebpwbc

frebpwbc2#

接受的答案对我不起作用,在graphql-request库的上下文中没有意义,它是可接受的参数,这是因为credentialsmode * 可以在header:对象的内部和外部,这不会有任何区别(下面解释)。
在本地测试OP的初始代码 * 没有任何更改 *,我没有看到任何问题。我看到Express sessionID从后端传递到前端。我确实看到了一个简单错误发生的可能性,尽管在开始和测试时...
在您的代码中,当您使用npm run dev启动前端时,它会在127.0.0.1:7000而不是localhost:7000上启动应用。当您使用npm run start启动后端时,它会在localhost:4000上启动应用。
在这种情况下,当您登录(或使用saveUninitializedtrue)时,GraphQL/Apollo Server不会将Express中的会话ID传递给客户端-因为您的域不同(127.0.0.1localhost)。
为了让GraphQL Express sessionID从后端cookie存储传播到前端cookie存储,您需要在本地使用相同的域。
例如,sessionID将使用以下组合正确传播

http://localhost:4000/graphql
http://localhost:7000/

http://127.0.0.1:4000/graphql
http://127.0.0.1:7000/

但是这里sessionID不会使用这些组合进行传播

localhost:4000/graphql
http://127.0.0.1:7000/

http://127.0.0.1:4000/graphql
http://localhost:7000/

这是很容易测试的,并且很明显是上述4种情况的情况。这让我想知道可接受的解决方案,因为它对我的OP代码的本地测试没有任何影响。
使用上面两个可接受的组合中的一个,我决定测试可接受的响应:

    • 前端/源/图形ql/图形qlClient. ts之前(正在运行)**:
import { GraphQLClient } from 'graphql-request';

export const graphqlClient = new GraphQLClient('http://localhost:4000/graphql', {
    headers: {
        'Content-Type': 'application/json',
        credentials: 'include',
        mode: 'cors',
    },
});
    • 前端/源/图形ql/图形ql客户端. ts之后(仍在工作...)**:
import { GraphQLClient } from 'graphql-request';

export const graphqlClient = new GraphQLClient('http://localhost:4000/graphql', {
    headers: {
        'Content-Type': 'application/json'
    },
    credentials: 'include',
    mode: 'cors',
});

那么为什么它仍然对我有效呢?那是因为根据交集类型定义,headers可以存在于多个区域中,并且库将处理它们。
右键单击上面代码中的headers:VSCode中的Go to Definition时,将看到以下内容:

export declare type PatchedRequestInit = Omit<Dom.RequestInit, 'headers'> & {
    headers?: MaybeFunction<Dom.RequestInit['headers']>;
    requestMiddleware?: RequestMiddleware;
    responseMiddleware?: (response: Response<unknown> | Error) => void;
};

注意PatchedRequestInit是一个交集类型吗?它有不止一个类型。它允许在不止一个地方设置header,而不是像公认的答案所建议的那样仅仅作为'header'的兄弟。

Omit<Dom.RequestInit, 'headers'>

`&` <--------------

{
    headers?: MaybeFunction<Dom.RequestInit['headers']>;
    requestMiddleware?: RequestMiddleware;
    responseMiddleware?: (response: Response<unknown> | Error) => void;
};

最后,当我们检查Dom.RequestInit时,在交集的BOTH部分,我们看到相同的类型定义。

export interface RequestInit {
    body?: BodyInit | null;
    cache?: RequestCache;
    credentials?: RequestCredentials;
    headers?: HeadersInit;
    integrity?: string;
    keepalive?: boolean;
    method?: string;
    mode?: RequestMode;
    redirect?: RequestRedirect;
    referrer?: string;
    referrerPolicy?: ReferrerPolicy;
    signal?: AbortSignal | null;
    timeout?: number;
    window?: any;
    fetch?: any;
    jsonSerializer?: JsonSerializer;
    /**
     * Decide how to handle GraphQLErrors in response
     */
    errorPolicy?: ErrorPolicy;
}

在我广泛的测试中,我还验证了cookie需要为localhost设置securefalse,否则它不会从后端传播到前端。

cookie: {
    maxAge: SEVEN_DAYS as number,
    sameSite: 'none',
    secure: false, // <--- must be false for localhost
},

最后,我怀疑OP在多次尝试后,在某个点上手动正确访问了localhost:7000,因为他们的应用程序在127.0.0.1:7000上自动启动有问题。然后他们尝试了将credentialsmode移出headers的"其他解决方案"。所以我怀疑OP认为第二件事解决了他的问题,而实际上第一件事解决了。它这是唯一合乎逻辑的解释。除非OP能解释为什么他们的代码从www.example.com开始!127.0.0.1!
基于我的研究,错误的答案被接受了,我承认我有点沮丧。我很高兴OP "让事情运转起来",但我希望未来的开发人员对"为什么"事情运转起来更感兴趣,慢慢地走,这样你就不会对正在发生的事情做出假设。

    • 编辑**

当在客户端:7000上并向服务器:4000提交一个空的登录表单以查看标题时,我看到credentials: includemode: cors,当这两个属性被放置在graphql-requestheaders:{}块中时。这是repo最初的方式。因此,这些标题被正确地设置。当您执行接受的答案,然后重复登录表单提交时,这些标题消失了**。这意味着如何设置credentialsmode的原始文档示例似乎是错误的。如果它们不应该在标题块中,为什么它们在块内部时出现,然后在块外部时消失,正如文档和接受的答案所建议的那样?在这种情况下,眼见为实。我看到标题在以"错误"的方式进行设置时被设置。

    • 编辑2**

查看前后的标题,自己判断放置credentialsmode属性的位置。
无任何变更:

    • 接受答案后,您实际上得到的结果与您想要的相反:**
x759pob2

x759pob23#

我不完全确定,但有几件事:
首先,set-cookie不会在您的cookie中向请求主机显示should fall back域,但这可能取决于您的浏览器
其次,根据the MDN doc设置“/”路径,这将与“/graphql”路径不匹配。
最后,你的屏幕看起来像Firefox,但你说你在“应用程序-〉Cookie”中搜索你的Cookie,而在现代的Firefox中,它应该在“存储-〉Cookie”中搜索

相关问题