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

Node/Express+MongoDB支付确认后更新产品库存的实现问题

购物车系统库存更新权限漏洞解决

问题背景

使用Node.js、Express和MongoDB开发购物车系统,集成Mercado Pago支付接口,需求是用户完成支付后自动更新所购产品的库存。但当前实现的updateProductsQuantity接口存在权限漏洞,任意登录用户都能通过修改请求参数中的userId来修改其他用户对应购物车的产品库存。

核心问题分析

  1. 权限校验缺失:仅通过verifyToken验证用户登录状态,但未校验当前登录用户(req.userId)与请求路径中的userId是否一致,恶意用户可篡改参数操作他人数据。
  2. 触发逻辑错误:将库存更新接口开放给前端调用,而非通过Mercado Pago的支付成功回调自动触发,增加了被滥用的风险。
  3. 非原子更新:未使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 09:05:22