Node/Express+MongoDB支付确认后更新产品库存的实现问题
购物车系统库存更新权限漏洞解决
问题背景
使用Node.js、Express和MongoDB开发购物车系统,集成Mercado Pago支付接口,需求是用户完成支付后自动更新所购产品的库存。但当前实现的updateProductsQuantity接口存在权限漏洞,任意登录用户都能通过修改请求参数中的userId来修改其他用户对应购物车的产品库存。
核心问题分析
- 权限校验缺失:仅通过
verifyToken验证用户登录状态,但未校验当前登录用户(req.userId)与请求路径中的userId是否一致,恶意用户可篡改参数操作他人数据。 - 触发逻辑错误:将库存更新接口开放给前端调用,而非通过Mercado Pago的支付成功回调自动触发,增加了被滥用的风险。
- 非原子更新:未使用MongoDB原子操作,并发场景下可能出现库存超卖问题。
解决方案
1. 调整触发逻辑:通过支付回调自动执行
库存更新不应由前端主动调用,而是在Mercado Pago返回支付成功通知时,由后端自动触发。
首先添加Mercado Pago的webhook路由:
// payment Route 新增回调接口 router.post("/webhook", mercadopagoWebhook);
2. 加强权限校验(若需保留手动触发接口)
如果必须保留手动触发接口,需强制校验当前登录用户ID与操作对象ID一致:
// Products route 修改路由,移除路径中的userId参数 router.put("/updateProductsQuantity", [verifyToken], updateProductsQuantity);
3. 原子性更新库存,避免超卖
使用MongoDB的$inc原子操作更新库存,同时提前校验库存充足性。
完整修改代码示例
支付回调控制器(处理Mercado Pago支付成功通知)
export const mercadopagoWebhook = async (req, res) => { try { const { query } = req; // 验证Mercado Pago签名,防止伪造请求 const isValid = mercadopago.webhook.validateRequest( req.body, query["x-signature"], process.env.MERCADOPAGO_WEBHOOK_SECRET ); if (!isValid) { return res.status(401).send("Invalid signature"); } // 仅处理支付成功事件 if (query.type === "payment" && req.body.action === "payment.created") { const payment = req.body.data; // 从元数据中获取关联的用户ID(创建订单时需存入) const userId = payment.metadata.userId; const cart = await Cart.findOne({ user: userId }).populate("products.id"); if (!cart || cart.products.length === 0) { return res.status(404).send("Cart not found"); } // 批量原子更新库存 const updateOperations = cart.products.map(item => ({ updateOne: { filter: { _id: item.id._id, stock: { $gte: item.quantity } }, // 校验库存充足 update: { $inc: { stock: -item.quantity } } // 原子减库存 } })); await Products.bulkWrite(updateOperations); // 清空购物车或标记订单完成 await Cart.findOneAndDelete({ user: userId }); res.status(200).send("Stock updated successfully"); } else { res.status(200).send("Event ignored"); } } catch (error) { console.error(error); res.status(500).send("Internal server error"); } };
修改后的updateProductsQuantity控制器(若保留手动触发)
export const updateProductsQuantity = async (req, res) => { try { // 强制校验当前登录用户ID const userId = req.userId; const cart = await Cart.findOne({ user: userId }).populate("products.id"); if (!cart || cart.products.length === 0) { return res.status(404).json({ msg: "no posee pedidos aun" }); } // 原子更新库存 const updateOperations = cart.products.map(item => ({ updateOne: { filter: { _id: item.id._id, stock: { $gte: item.quantity } }, update: { $inc: { stock: -item.quantity } } } })); const result = await Products.bulkWrite(updateOperations); // 检查是否有产品库存不足 if (result.modifiedCount !== cart.products.length) { return res.status(400).json({ msg: "部分产品库存不足" }); } // 清空购物车 await Cart.findOneAndDelete({ user: userId }); res.json({ msg: "库存更新成功" }); } catch (error) { console.error(error); res.status(500).send("hubo un error"); } };
调整createOrder控制器,添加用户ID到元数据
export const createOrder = async (req, res) => { const user = await User.findById(req.userId).select('-password'); const cart = await Cart.findOne({ user: user._id }).populate({ path: "products.id", model: "Productos" }); let preference = { items: [], back_urls: { "success": "http://localhost:3000/notification", "failure": "http://localhost:3000/notification", "pending": "http://localhost:3000/notification" }, auto_return: "approved", metadata: { userId: user._id.toString() } // 存入用户ID,用于回调识别 }; cart.products.forEach(product => { preference.items.push({ title: product.id.name, quantity: product.quantity, currency_id: 'ARS', unit_price: product.id.price }) }) const response = await mercadopago.preferences.create(preference); const preferenceId = response.body.id; res.send({ preferenceId }); };
关键注意事项
- 必须验证Mercado Pago webhook的签名,防止伪造支付通知。
- 库存更新使用
bulkWrite结合$inc原子操作,避免并发超卖。 - 支付成功后建议将购物车转换为正式订单记录,便于后续对账和查询。
内容的提问来源于stack exchange,提问作者kirinico
相关产品推荐
相关产品推荐

