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或字符串)——签名是基于原始请求内容生成的,解析后的对象和原始内容的格式、编码存在差异,直接导致验证失败。
解决方案
- 调整中间件加载顺序:在全局解析中间件之前,先处理Webhook路由,避免Webhook请求被提前解析。
- 为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
相关产品推荐
相关产品推荐

