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

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流的库无法正常工作,同时签名验证时因为请求体处理不正确而失败。

分步解决方案

  1. 修正请求类型与获取原始请求体
    替换NextApiRequest为Web标准Request,用req.text()获取原始请求体字符串(Stripe签名验证需要未修改的原始payload)。
  2. 正确提取Stripe签名头
    使用Next.js App Router的headers()函数获取请求头,因为Request的headers是Headers对象,而非普通键值对。
  3. 移除无效的Pages Router配置
    App Router不需要api: { bodyParser: false }这类配置,默认不会解析请求体,无需额外设置。
  4. 确保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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 09:44:52