Next.js集成Stripe Webhook签名验证失败问题求助
解决Stripe Webhook「No signatures found matching the expected signature for payload」及请求体获取错误
核心问题原因
你在Next.js App Router的API路由里错误使用了Pages Router的NextApiRequest类型,App Router的请求对象是Web标准的Request,而非Node.js风格的流对象,导致micro、raw-body这类依赖Node流的库无法正常工作,同时签名验证时因为请求体处理不正确而失败。
分步解决方案
- 修正请求类型与获取原始请求体
替换NextApiRequest为Web标准Request,用req.text()获取原始请求体字符串(Stripe签名验证需要未修改的原始payload)。 - 正确提取Stripe签名头
使用Next.js App Router的headers()函数获取请求头,因为Request的headers是Headers对象,而非普通键值对。 - 移除无效的Pages Router配置
App Router不需要api: { bodyParser: false }这类配置,默认不会解析请求体,无需额外设置。 - 确保Stripe密钥正确
确认使用的是Webhook签名密钥(在Stripe控制台Webhook设置里获取,以whsec_开头),而非普通API密钥;同时避免用NEXT_PUBLIC_前缀暴露敏感密钥到前端。
修正后的完整代码
import Cors from "micro-cors"; import { headers } from "next/headers"; import { NextResponse } from "next/server"; import Stripe from "stripe"; import prisma from "@/prisma/utils"; import { Users, Orders } from '@prisma/client'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const cors = Cors({ allowMethods: ["POST", "HEAD"], }); const secret = process.env.STRIPE_WEBHOOK_SECRET || ""; export async function POST(req: Request) { try { // 获取原始请求体字符串 const rawBody = await req.text(); // 获取Stripe签名头 const headersList = headers(); const sig = headersList.get("stripe-signature")!; // 验证签名并构造事件 const event = stripe.webhooks.constructEvent(rawBody, sig, secret); if (event.type === "checkout.session.completed") { const session = event.data.object as Stripe.Checkout.Session; if (!session.customer_email) { throw new Error(`缺失用户邮箱,事件ID: ${event.id}`); } const id = session.id; const createdAt = new Date(session.created * 1000); const existingOrder = await prisma.orders.findUnique({ where: { id }, }); if (existingOrder) { throw new Error(`订单已存在,ID: ${id}`); } const email = session.customer_details?.email!; const user = await prisma.users.findUnique({ where: { email }, }); if (!user) { throw new Error(`未找到用户,邮箱: ${email}`); } // 获取订单项 const lineItems = await stripe.checkout.sessions.listLineItems(id); const products = JSON.stringify( lineItems.data.map(item => ({ item: item.description || item.price?.product?.name, quantity: item.quantity, price: item.price?.unit_amount_decimal })) ); // 创建订单 await prisma.orders.create({ data: { id, createdAt, user_id: user.id, user_name: session.customer_details?.name!, address: session.customer_details?.address?.line1!, address_2: session.customer_details?.address?.line2 || "", city: session.customer_details?.address?.city!, state: session.customer_details?.address?.state!, zip: Number(session.customer_details?.address?.postal_code!), profit: Number(session.amount_total), shipped: false, delivered: false, // 修正原代码拼写错误 products, }, }); } return NextResponse.json({ result: event, ok: true }); } catch (error) { console.error("Webhook处理错误:", error); // 签名验证失败返回400而非500,符合HTTP规范 return NextResponse.json( { message: "Webhook处理失败", ok: false }, { status: 400 } ); } }
额外注意事项
- 空值处理:代码中添加了对
customer_details、address等可选字段的空值兼容,避免运行时崩溃。 - 测试建议:使用Stripe CLI测试Webhook,确保请求体和签名是Stripe官方生成的,手动构造请求易导致签名不匹配。
内容的提问来源于stack exchange,提问作者lorenzo washington
相关产品推荐
相关产品推荐

