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

部署DigitalOcean后NextJS+Express的JWT Cookie不携带问题排查

JWT Cookie在DigitalOcean部署后无法携带的问题

我用NextJS做前端、Node/ExpressJS做后端搭建项目,注册或登录时会生成JWT Cookie并配置到前端。本地环境所有功能正常,但后端部署为DigitalOcean App后,前端发起的请求似乎未携带该Cookie。登录注册能正常完成,但所有需要验证Cookie的请求均失败,返回错误:POST https://xxxxx.ondigitalocean.app/api/v1/item/ 401 (Unauthorized),错误信息为"No token provided"。我试过调整Cookie的secure、httpOnly、sameSite等配置项,但都没解决问题。


Auth控制器代码

import bcrypt from "bcrypt";
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import User from "../models/userModel.js";
import {
  generateOTP,
  resendOTP,
  sendOTP,
  validateOTP,
} from "../utils/nodemailer.js";

const JWT_SECRET = process.env.JWT_SECRET!;

export const registerController = async (req: Request, res: Response) => {
  const { firstName, lastName, email, password } = req.body;

  if (!email.endsWith("@bitmesra.ac.in")) {
    return res.status(400).json({ message: "Invalid email domain" });
  }

  try {
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(400).json({ message: "User already exists" });
    }

    const hashedPassword = await bcrypt.hash(password, 10);

    const otp = generateOTP();
    const newUser = new User({
      firstName,
      lastName,
      email,
      hashedPassword,
      otp,
      isVerified: false,
      otpRequestDate: new Date(),
      otpRequestCount: 1,
    });

    await newUser.save();
    await sendOTP(email, otp);

    res.status(201).json({
      message: "User registered. OTP sent to email.",
      user: { _id: newUser._id },
    });
  } catch (error) {
    res.status(500).json({ message: "Error registering user", error });
  }
};

export const verifyOtpController = async (req: Request, res: Response) => {
  const userId = req.params.id;
  const { otp } = req.body;
  console.log("User ID:", userId);
  console.log("OTP:", otp);

  try {
    const isValid = await validateOTP(userId, otp);
    if (!isValid) {
      return res.status(400).json({ message: "Invalid OTP" });
    }

    const user = await User.findById(userId);
    if (user) {
      user.isVerified = true;
      user.otp = undefined;
      await user.save();

      const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET!, {
        expiresIn: "7d",
      });

      res
        .cookie("token", token, {
          httpOnly: true,
          secure: true,
          sameSite: "none",
          domain: "king-prawn-app-amyex.ondigitalocean.app",
        })
        .status(200)
        .json({
          message: "Account verified successfully",
          user: {
            firstName: user.firstName,
            lastName: user.lastName,
            email: user.email,
            _id: user._id,
            token: token,
          },
        });
    } else {
      res.status(404).json({ message: "User not found" });
    }
  } catch (error) {
    res.status(500).json({ message: "Error verifying OTP", error });
  }
};

export const loginController = async (req: Request, res: Response) => {
  const { email, password } = req.body;
  try {
    if (!email || !password) {
      return res.status(400).json({ message: "Please fill in all fields" });
    }

    if (!email.endsWith("@bitmesra.ac.in")) {
      return res.status(400).json({ message: "Invalid email domain" });
    }
    const user = await User.findOne({ email }).select("+hashedPassword");
    if (!user) {
      return res.status(404).json({ message: "Invalid Email or Password" });
    }

    const isMatch = await bcrypt.compare(password, user.hashedPassword!);
    if (!isMatch) {
      return res.status(400).json({ message: "Invalid Email or Password" });
    }

    if (!user.isVerified) {
      const otp = generateOTP();
      user.otp = otp;
      user.otpLastRequestedAt = new Date();
      await user.save();

      await sendOTP(email, otp);

      return res.status(403).json({
        message:
          "Your account is not verified. A verification email has been sent to your email address.",
        userId: user._id,
      });
    }
    const token = jwt.sign({ id: user._id }, JWT_SECRET, { expiresIn: "7d" });
    res
      .cookie("token", token, {
        httpOnly: true,
        secure: true,
        sameSite: "none",
        domain: "king-prawn-app-amyex.ondigitalocean.app",
      })
      .status(200)
      .json({
        firstName: user.firstName,
        lastName: user.lastName,
        email: user.email,
        _id: user._id,
        token,
        message: "Logged in successfully",
      });
  } catch (error) {
    res.status(500).json({ message: "Error logging in", error });
  }
};

物品控制器代码

export const createItem = async (req: Request, res: Response) => {
  try {
    const userId = req.user?.id;
    const {
      title,
      description,
      price,
      room_no,
      hostel_no,
      year_of_purchase,
      category,
      contact_no,
    } = req.body;

    const existingItem = await Item.findOne({ title, seller: userId });
    if (existingItem) {
      return res
        .status(400)
        .json({ message: "You already have an item with this title" });
    }

    // 计算总文件大小
    const totalSize = (req.files as Express.Multer.File[]).reduce(
      (acc: number, file: Express.Multer.File) => acc + file.size,
      0
    );
    if (totalSize > MAX_FILE_SIZE) {
      return res
        .status(400)
        .json({ message: "Total file size exceeds the 25 MB limit." });
    }

    // 调整图片尺寸并上传
    const imageDetails = [];
    for (const file of req.files as Express.Multer.File[]) {
      const buffer = await sharp(file.buffer).toFormat("webp").toBuffer();
      const imageName = randomImageName();
      const params = {
        Bucket: bucketName!,
        Key: imageName,
        Body: buffer,
        ContentType: file.mimetype,
      };
      const command = new PutObjectCommand(params);
      await s3.send(command);
      imageDetails.push({ key: imageName });
    }

    const newItem = new Item({
      title,
      description,
      price,
      room_no,
      hostel_no,
      year_of_purchase,
      category,
      seller: userId,
      images: imageDetails,
      contact_no,
    });
    await newItem.save();

    await User.findByIdAndUpdate(userId, { $push: { items: newItem._id } });

    res.status(201).json({ message: "Item created Successfully", newItem });
  } catch (error) {
    res.status(500).json({ message: "Server error", error });
  }
};

路由配置代码

import { Router } from "express";
import { uploadMiddleware } from "../config/multerConfig.js";
import {
  createItem,
  deleteImage,
  deleteItem,
  getAllItems,
  getItemById,
  getItems,
  updateImages,
  updateItem,
} from "../controllers/itemController.js";
import { authMiddleware } from "../middlewares/authMiddleware.js";

const router = Router();

router.get("/all", getAllItems);
router.use(authMiddleware);
router.route("/").post(uploadMiddleware, createItem).get(getItems);

router.route("/:id").get(getItemById).put(updateItem).delete(deleteItem);
router.patch("/:id/images", uploadMiddleware, updateImages);
router.delete("/:itemId/images/:imageId", deleteImage);

export default router;

认证中间件代码

import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";

const JWT_SECRET = process.env.JWT_SECRET!;

export const authMiddleware = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const token = req.cookies.token;
  // console.log("token:", token);
  if (!token) {
    return res.status(401).json({ message: "No token provided" });
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
    //console.log("decoded:", decoded);
    req.user = { id: decoded.id };
    next();
  } catch (error) {
    return res.status(401).json({ message: "Invalid token" });
  }
};

排查与解决方案

1. Cookie域名配置问题

当前硬编码了Cookie的domain为king-prawn-app-amyex.ondigitalocean.app,若前端部署在不同域名下,会导致Cookie无法被携带。建议:

  • 移除domain配置,让浏览器自动根据当前请求域名设置Cookie;
  • 或根据环境变量动态设置域名,比如生产环境用DigitalOcean域名,本地用localhost。

2. 跨域请求Credentials配置

前端发起请求时必须设置携带凭证:

  • 使用fetch时,添加credentials: 'include';
  • 使用Axios时,设置withCredentials: true。

3. CORS配置缺失

后端需正确配置CORS,允许携带凭证:

app.use(cors({
  origin: [你的前端域名], // 不能设为*,必须指定具体域名
  credentials: true
}));

4. Secure Cookie环境适配

secure: true要求请求为HTTPS,本地开发用HTTP时会导致Cookie无法设置,建议根据环境动态切换:

secure: process.env.NODE_ENV === 'production'

5. SameSite配置验证

sameSite: 'none'必须配合secure: true使用,生产环境下需同时设置,否则浏览器可能拒绝Cookie。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 17:45:55