部署DigitalOcean后NextJS+Express的JWT Cookie不携带问题排查
我用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
相关产品推荐
相关产品推荐

