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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 02:36:03