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

Express.js集成Stripe Webhook时签名验证失败问题排查

Express.js集成Stripe Webhook签名验证失败问题解决

错误信息

⚠️ Webhook signature verification failed. Webhook payload must be provided as a string or a Buffer (https://nodejs.org/api/buffer.html) instance representing the raw request body.Payload was provided as a parsed JavaScript object instead. Signature verification is impossible without access to the original signed material.

当前配置

index.js

require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const port = 8080;
const cors = require("cors");
const mongoose = require("mongoose");

app.use(cors());

app.get("/", (req, res) => {
  res.send("Hello, World!");
});

// Use these parsers for other routes
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));

const dbURI = process.env.MONGODB_CONNECTION;

const searchRouter = require("./routes/searchRoutes");
const phoneRouter = require("./routes/phone");
const paymentRouter = require("./routes/payment");
const linkRouter = require("./routes/link");
const pieceRouter = require("./routes/piece");
const userRouter = require("./routes/user");
const emailRouter = require("./routes/email");
const autocompleteRouter = require("./routes/autocomplete");
const webhookRouter = require("./routes/webhook");

app.use("/", searchRouter);
app.use("/phone", phoneRouter);
app.use("/payment", paymentRouter);
app.use("/link", linkRouter);
app.use("/piece", pieceRouter);
app.use("/user", userRouter);
app.use("/email", emailRouter);
app.use("/autocomplete", autocompleteRouter);
app.use("/webhook", webhookRouter);

mongoose
  .connect(dbURI)
  .then(() => {
    console.log("MongoDB connected");
    app.listen(port, () => {
      console.log(`Server running on http://localhost:${port}/`);
    });
  })
  .catch((err) => console.log(err));

webhook.js

require("dotenv").config();
const express = require("express");
const router = express.Router();
const stripe = require("stripe")(process.env.STRIPE_SECRET_TEST_KEY);

const endpointSecret = process.env.STRIPE_TEST_WEBHOOK_KEY;

router.post("/", async (req, res) => {
  const sig = req.headers["stripe-signature"];

  console.log("Webhook received!");

  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
  } catch (err) {
    console.log(`⚠️  Webhook signature verification failed.`, err.message);
    return res.sendStatus(400);
  }

  if (event.type === "checkout.session.completed") {
    const session = event.data.object;

    try {
      const paymentIntent = await stripe.paymentIntents.retrieve(
        session.payment_intent,
        {
          expand: ["charges"],
        }
      );

      const charge = paymentIntent.charges.data[0];
      const transferId = charge.transfer;
      const taxAmount = session.total_details.amount_tax;

      await stripe.transfers.createReversal(transferId, {
        amount: taxAmount,
      });

      console.log(`Successfully reversed tax amount: ${taxAmount}`);
    } catch (error) {
      console.error("Error creating transfer reversal:", error);
    }
  }

  res.sendStatus(200);
});

module.exports = router;

问题原因与解决步骤

核心问题

全局启用的express.json()中间件会把所有POST请求的原始JSON体解析成JavaScript对象,而Stripe签名验证依赖原始未解析的请求体(Buffer或字符串)——签名是基于原始请求内容生成的,解析后的对象和原始内容的格式、编码存在差异,直接导致验证失败。

解决方案

  1. 调整中间件加载顺序:在全局解析中间件之前,先处理Webhook路由,避免Webhook请求被提前解析。
  2. 为Webhook配置原始体解析:使用express.raw()中间件获取原始请求体,替代JSON解析。

修改后的index.js

require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const port = 8080;
const cors = require("cors");
const mongoose = require("mongoose");

app.use(cors());

app.get("/", (req, res) => {
  res.send("Hello, World!");
});

// 先加载webhook路由,用raw中间件保留原始请求体
const webhookRouter = require("./routes/webhook");
app.use("/webhook", express.raw({ type: "application/json" }), webhookRouter);

// 再为其他路由启用JSON和urlencoded解析
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));

const dbURI = process.env.MONGODB_CONNECTION;

const searchRouter = require("./routes/searchRoutes");
const phoneRouter = require("./routes/phone");
const paymentRouter = require("./routes/payment");
const linkRouter = require("./routes/link");
const pieceRouter = require("./routes/piece");
const userRouter = require("./routes/user");
const emailRouter = require("./routes/email");
const autocompleteRouter = require("./routes/autocomplete");

app.use("/", searchRouter);
app.use("/phone", phoneRouter);
app.use("/payment", paymentRouter);
app.use("/link", linkRouter);
app.use("/piece", pieceRouter);
app.use("/user", userRouter);
app.use("/email", emailRouter);
app.use("/autocomplete", autocompleteRouter);

mongoose
  .connect(dbURI)
  .then(() => {
    console.log("MongoDB connected");
    app.listen(port, () => {
      console.log(`Server running on http://localhost:${port}/`);
    });
  })
  .catch((err) => console.log(err));

webhook.js无需额外修改

stripe.webhooks.constructEvent可以直接处理Buffer类型的原始请求体,现有逻辑完全兼容。如果后续需要手动解析JSON,可添加如下代码:

const rawBodyStr = req.body.toString();
const parsedBody = JSON.parse(rawBodyStr);

额外注意事项

  • ngrok不会影响签名验证,只要Stripe能正常发送请求到你的ngrok端点即可。
  • 确保STRIPE_TEST_WEBHOOK_KEY与Stripe Dashboard中配置的签名密钥完全一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 20:35:03