NextJS中Stripe Webhook获取raw body签名验证失败如何解决?
核心原因说明
你遇到的签名验证失败报错,本质是传递给stripe.webhooks.constructEvent的payload和Stripe发出的原始请求body不一致,要么是被NextJS默认的bodyParser解析修改了格式,要么是Webhook密钥配置错误、测试方式不符合要求。
修复方案(分NextJS路由模式)
1. Pages Router (你现有代码的修复版)
你第二版代码已经关闭了bodyParser,只需要调整几个细节即可:
buf.toString()指定utf8编码,避免编码差异- 确认Webhook密钥无配置错误
- 移除不必要的日志避免干扰
修复后代码如下:
import { buffer } from 'micro'; import Cors from 'micro-cors'; const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); const webhookSecret = process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET_NEW; // 必须关闭默认bodyParser,才能获取原始请求body export const config = { api: { bodyParser: false, }, }; const cors = Cors({ allowMethods: ['POST', 'HEAD'], }); const webhookHandler = async (req, res) => { if (req.method !== 'POST') { res.setHeader('Allow', 'POST'); return res.status(405).end('Method Not Allowed'); } try { const buf = await buffer(req); const rawBody = buf.toString('utf8'); const sig = req.headers['stripe-signature']; const event = stripe.webhooks.constructEvent( rawBody, sig, webhookSecret ); // 处理事件逻辑 switch (event.type) { case 'payment_intent.succeeded': const paymentIntent = event.data.object; console.log(`支付成功,ID:${paymentIntent.id}`); break; case 'payment_intent.payment_failed': const failedIntent = event.data.object; console.log(`支付失败:${failedIntent.last_payment_error?.message}`); break; case 'charge.succeeded': const charge = event.data.object; console.log(`扣费成功,Charge ID:${charge.id}`); break; default: console.log(`未处理的事件类型:${event.type}`); } return res.status(200).json({ received: true }); } catch (err) { console.log('Webhook验证失败:', err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } }; export default cors(webhookHandler);
2. App Router(NextJS 13+ 适用)
如果使用App Router,Pages Router的config配置不生效,直接读取原始请求流即可,示例代码如下:
import { NextResponse } from 'next/server'; import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); const webhookSecret = process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET_NEW; export async function POST(request) { try { const rawBody = await request.arrayBuffer(); const sig = request.headers.get('stripe-signature'); const event = stripe.webhooks.constructEvent( Buffer.from(rawBody), sig, webhookSecret ); // 处理事件逻辑和上面一致 switch (event.type) { case 'payment_intent.succeeded': console.log('支付成功'); break; // 其他事件处理 } return NextResponse.json({ received: true }, { status: 200 }); } catch (err) { return NextResponse.json({ error: err.message }, { status: 400 }); } }
必须确认的配置&测试规则
- Webhook密钥不能和Stripe普通的秘钥混淆:生产环境密钥在Stripe后台「Webhooks」对应端点的「签名密钥」处获取;本地测试必须使用
stripe listen命令输出的webhook signing secret,不要直接用生产密钥。 - 本地测试必须使用Stripe CLI触发事件:执行
stripe listen --forward-to localhost:3000/api/你的webhook路径,再执行stripe trigger payment_intent.succeeded触发测试事件,不要用Postman等工具手动构造请求,手动构造的payload签名必然不匹配。 - 检查环境变量配置:粘贴Webhook密钥时不要带多余的空格、换行符,部署到Vercel等平台时要确认环境变量已经正确更新。
- 不要在Webhook端点上添加任何会修改请求body的中间件,比如请求体解析、格式转换类的插件。
内容的提问来源于stack exchange,提问作者SteveW
相关产品推荐
相关产品推荐

