reactjs 即使在浏览器中设置了cookie,expressreq.cookies也会返回一个空对象

disho6za  于 2023-10-17  发布在  React
关注(0)|答案(1)|浏览(110)

我正在尝试使用Express、JWT和Prisma创建一个基本的身份验证系统。我的前端是用Next/React写的。我想使用仅限HTTP的Cookie执行JWT验证。当用户最初从前端登录到他们的帐户时,令牌cookie会成功创建(我使用浏览器的开发工具确认了这一点)。每当我尝试使用req.cookiescookie-parser验证JWT时,它总是返回一个空对象[Object: null prototype] {},即使在确认浏览器中已设置cookie之后也是如此。
我的express应用程序是这样配置的(注意:我在调试时将所有内容压缩到一个文件中):
Front-end: http://localhost:3000Back-end: http://localhost:5000

import { Express } from "express";
import { matchedData, validationResult, body } from "express-validator"
// import memberdata from "./api/memberdata.js"
// import auth from "./auth/auth.js"
import express from "express";
import cookieParser from "cookie-parser"
import cors from "cors";
import prisma from "./client.js"
import jwt from "jsonwebtoken"
import dotenv from "dotenv"

dotenv.config()

const app:Express = express();
const port = 5000;

app.use(cookieParser());
app.use(cors({
    credentials: true,
    origin: "http://localhost:3000"
}));
app.use(express.json());

// initial authentication route

app.post("/auth", body("email").isEmail(), body("password").exists(), async (req, res, next) => {

    const result = validationResult(req)
    if (result.isEmpty()) {
        
        const { email, password } = matchedData(req);

        const user = await prisma.member.findUnique({
            where: {
                email: email
            }
        })

        if (user) {

            if (password != user.password) {
                const error = Error("Incorrect credentials.")
                return next(error)
            }

            const token = jwt.sign({email: email}, process.env.TOKEN_SECRET as string, { expiresIn: "30m" })

            res.cookie("token", token, {
                secure: false,
                httpOnly: false,
                maxAge: 6 * 60 * 60 * 1000
            }).status(200);
            return res.json("Cookie has been set")

        } else return null;
        
    }

    res.send({ errors: result.array() })

})

// dummy API route

app.get("/api", (req, res) => {
    res.send("API Page")
})

// memberdata API route

app.get("/api/memberdata", async (req, res) => {

    // begin token verification
    // usually a separate function, but explicitly stated here for debugging

    const token = req.cookies.token;
    console.log(req.cookies);

    if (!token) return res.status(401).json("You need to login!")

    jwt.verify(token, process.env.TOKEN_SECRET as string, (err: any) => {
        console.log(err);
        if (err) return res.status(403);
    })

    // end token verification

    const useremail = "[email protected]";
    const memberdata = await prisma.member.findUnique({
        where: {
            email: useremail
        },
        select: {
            social: true,
            health: true,
            philanthropy: true,
            development: true,
            fundraising: true,
            events: true
        }
    });

    return res.json(memberdata);

})

app.listen(port, () => {
    console.log(`Express Server listening on port ${port}`);
});

当初始身份验证路由被调用时,cookie会在浏览器中成功设置。当/api/memberdata路由被调用时,它根据第一个条件返回,因为它检测到req.cookies是一个空对象。我尝试了以下方法:

  • CORS中的credentials: true
  • 先调用cookie-parser中间件
  • 在我的前端获取请求中包含credentials: 'include'(见下文)

以下是我的前端请求,供参考:

// initial authentication when the user logs in for the first time

    async function onSubmit(event: FormEvent<HTMLFormElement>) {
        event.preventDefault();
        
        const formData = new FormData(event.currentTarget);
        const formDataJSON = JSON.stringify(Object.fromEntries(formData.entries()));

        const response = await ( await fetch("http://localhost:5000/auth", {
            method: 'POST',
            credentials: 'include',
            body: formDataJSON,
            headers: {
                "Content-Type": "application/json"
            },
        })).json()

    }
// fetch to the api memberdata route 

async function fetchData(): Promise<{
  social: number,
  health: number,
  philanthropy: number,
  development: number,
  fundraising: number,
  events: {title: string}[]
} | null > {
  const response = await (await fetch("http://localhost:5000/api/memberdata", {
    method: "GET",
    cache: "no-store",
    credentials: 'include',
    headers: {
      "Content-Type": "application/json"
  },
  })).json()
  return response
}

更新1

在对这些请求进行了一番研究之后,我发现了以下几点:
让我们假设一个用户已经登录,并且令牌cookie被发送到浏览器。目前,如果用户尝试访问受身份验证保护的API路由,令牌验证器函数将返回,因为req. cookie对象为空。另请注意:

  • 每当向/api/memberdata发送请求时,令牌cookie都会出现在开发人员工具的Network和Application选项卡中的请求标头下。

如果我从浏览器对API路由的请求中提取这个令牌,并将其插入到Postman GET请求中,该请求将毫无问题地通过验证器函数,并返回所请求的数据。

qlvxas9a

qlvxas9a1#

从上面的评论中,听起来像是当您从浏览器向API直接请求/auth路由时,您正在向Next.js API route请求/api/memberdata

+-------+                 +-------+                 +---------+
| React |                 | Next  |                 | Express |
+-------+                 +-------+                 +---------+
    |                         |                          |
    | POST /auth              |                          |
    |--------------------------------------------------->|
    |                         |                          |
    |                         |         Set-Cookie token |
    |<---------------------------------------------------|
    |                         |                          |
    | GET /api/memberdata     |                          |
    |------------------------>|                          |
    |                         |                          |
    |                         | GET /api/memberdata      |
    |                         |------------------------->|
    |                         |                          |
    |                         |                 HTTP 403 |
    |                         |<-------------------------|
    |                         |                          |

Cookie将仅为您的Express服务器域http://localhost:5000设置。它们不会被包含在对本地Next.js API路由的请求中,而且,无论如何,您都不会将它们添加到Express中。
最简单的解决方案是继续直接向Express API发出经过身份验证的请求。
所以你可能在客户端代码中有这样的东西.

fetch("/api/memberdata", { credentials: "include" })

改成这个

fetch("http://localhost:5000/api/memberdata", { credentials: "include" })
  • 这是最初的答案,但它仍然包含有关跨域cookie身份验证的有用信息,特别是在生产模式下。

为了支持跨域Cookie,您需要配置Cookie选项以包含SameSite属性。
对于客户端和服务器都运行在同一主机名(即localhost)上的开发环境,您可以使用“Lax”,这也是默认值。

res.cookie("token", token, {
  httpOnly: true, // 👈 client side JS should not have access
  maxAge: 6 * 60 * 60 * 1000,
  secure: false,
  sameSite: "Lax",
});

对于生产模式,您需要启用HTTPS的服务器和SameSite=None; Secure

res.cookie("token", token, {
  httpOnly: true,
  maxAge: 6 * 60 * 60 * 1000,
  secure: true,
  sameSite: "None",
});

您可以将这些合并与process.env.NODE_ENV上的检查结合起来,以使代码更易于在任一环境中使用。

const isProd = process.env.NODE_ENV === "production";

res.cookie("token", token, {
  httpOnly: true,
  maxAge: 6 * 60 * 60 * 1000,
  secure: isProd,
  sameSite: isProd ? "None" : "Lax",
});

相关问题