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

服务器端Stripe Webhook签名匹配失败,本地运行正常

Stripe Webhook签名验证失败问题排查与解决

问题描述

本地环境运行Stripe Webhook完全正常,但服务器端持续收到以下错误:

No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe?
If a webhook request is being forwarded by a third-party tool, ensure that the exact request body, including JSON formatting and new line style, is preserved.

已确认:

  • 请求体已按原始状态传入路由
  • 请求能到达正确端点(仅该端点触发错误)
  • 使用Stripe官方库处理Webhook

后端相关代码

const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const bodyParser = require('body-parser');

const app = express();

app.use((req, res, next) => {
  // Check if the request is for the '/webhook' endpoint
  console.log(req.originalUrl);
  if (req.originalUrl === '/api/webhook') {
    // Continue to the next middleware or route handler
    next();
  } else {
    // If not a '/webhook' request, use the express.json() middleware
    express.json()(req, res, next);
  }
});

app.post('/api/webhook', bodyParser.raw({ type: 'application/json' }), async (req, res) => {
  const endpointSecret = process.env.STRIPE_SECRET_KEY;
  const sig = req.headers['stripe-signature'];

  let event;

  try {
    if (!endpointSecret) {
      return res.status(400).send('Webhook Error: Missing secret key');
    }

    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);

    switch (event.type) {
      case 'checkout.session.completed':
        const session = event.data.object;
        if (!session.metadata) return;

        const metaData = JSON.parse(session.metadata.invoiceInfo.toString());
        if (session.payment_status === 'paid') {
          const redisData = await RedisManager.Get(`${RedisKey.StripPayment}_${session.id}_${metaData.userId}_${metaData.invoiceId}`);
          const data = await Unitofwork.invoiceService.GetInvoiceByIdForPayment(redisData?.id);

          const comission = data.platformComission;
          let credit = ((data.creditPercent || 0) / 100) * comission;
          let platformComission = (comission + (Number(data.networkFee) * Number(data.netwrokExchangeRate))) + credit;

          let amount = await this.calculatePrice(data.price, data.discount);

          const hash = await this.transferSameToken(redisData?.payBy._id, data.reciverAddressWallet, amount, platformComission, true);

          if (typeof hash == 'object') {
            data.transactionHash = hash.transactionTxn;
            data.platformTransactionHash = hash.platformTxn;
            data.message = 'invoice.message_successfull';
            data.status = InvoiceStatus.Success;
            await new CreditUtil().updateUserCredit(data, redisData?.payBy._id);
            data.save();

            await CpayNotification.sendNotification(data, true);

            await RedisManager.Remove(`${RedisKey.StripPayment}_${session.id}_${metaData.userId}_${metaData.invoiceId}`);
          }
        }
        break;
      case 'payment_intent.succeeded':
        const paymentIntent = event.data.object;
        break;
      case 'payment_method.attached':
        const paymentMethod = event.data.object;
        break;
      default:
        console.log(`Unhandled event type ${event.type}.`);
    }

    res.status(200).send();
  } catch (err) {
    console.log(err);
    res.status(400).send(`Webhook Error: ${err.message}`);
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

补充说明

  • 代码本地运行正常,怀疑Nginx在请求到达应用前修改了请求体

当前Nginx配置

server {
    listen 80;
    server_name gateway-pay.com;

    if ($host = gateway-pay.com) {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name xxx.com;

    ssl_certificate /etc/nginx/ssl/cloudflare-csr.pem;
    ssl_certificate_key /etc/nginx/ssl/cloudflare-key.pem;

    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 1440m;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers 'xxxxx';

    # Security headers
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Content-Security-Policy "frame-ancestors 'self'";

    # Uncomment if you want to enforce HSTS
    # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' '*' ;
            add_header 'Access-Control-Allow-Methods' '*' ;
            add_header 'Access-Control-Allow-Headers' '*' ;
            add_header 'Access-Control-Allow-Credentials' 'true' always ;
            return 204;
        }

        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://localhost:4200;
    }

    location /api {
        if ($request_method = OPTIONS) {
            add_header 'Access-Control-Allow-Origin' '*' ;
            add_header 'Access-Control-Allow-Methods' '*' ;
            add_header 'Access-Control-Allow-Headers' '*' ;
            add_header 'Access-Control-Allow-Credentials' 'true' always ;
            return 204;
        }

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://localhost:3001;
    }

    location /socket.io {
        proxy_pass http://localhost:3001/socket.io;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }

    location /api/webhook {
        proxy_pass http://localhost:3001;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;

        # Disable request buffering to ensure the raw request is passed through
        proxy_request_buffering off;
        proxy_buffering off;
    }

    # Uncomment and adjust if needed
    # location /currency-* {
    #     proxy_pass http://localhost:3001;
    #     proxy_http_version 1.1;
    #     proxy_set_header Upgrade $http_upgrade;
    #     proxy_set_header Connection "Upgrade";
    #     proxy_set_header Host $host;
    # }
}

解决方案

1. 修正Nginx路由匹配与路径问题

当前/api路由优先级高于/api/webhook,导致Webhook请求会先进入/api块,可能被CORS处理或其他设置干扰。调整路由顺序,并修正proxy_pass路径:

# 优先匹配Webhook路由
location /api/webhook {
    proxy_pass http://localhost:3001/api/webhook;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;

    # 禁止缓冲和修改请求体
    proxy_request_buffering off;
    proxy_buffering off;
    proxy_set_header Content-Length $content_length;
    proxy_http_version 1.1;
}

# 再定义通用/api路由
location /api {
    if ($request_method = OPTIONS) {
        add_header 'Access-Control-Allow-Origin' '*' ;
        add_header 'Access-Control-Allow-Methods' '*' ;
        add_header 'Access-Control-Allow-Headers' '*' ;
        add_header 'Access-Control-Allow-Credentials' 'true' always ;
        return 204;
    }

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://localhost:3001;
}

2. 确保Nginx不修改请求体

  • 启用proxy_request_buffering off;和proxy_buffering off;,防止Nginx缓冲并修改请求内容
  • 添加proxy_set_header Content-Length $content_length;,保留原始请求的Content-Length头部,避免Nginx重新计算
  • 修正proxy_pass路径:原配置会丢失/api/webhook路径,改为完整路径确保后端收到正确的请求端点

3. 后端代码纠错

检查endpointSecret取值:当前代码使用STRIPE_SECRET_KEY(API密钥),但Stripe Webhook签名需要使用专属的Webhook Secret,而非API密钥。确认环境变量已配置正确的Webhook Secret。

4. 验证请求体一致性

在Webhook端点添加日志,对比本地与服务器端的请求体内容和长度,确认是否被修改:

app.post('/api/webhook', bodyParser.raw({ type: 'application/json' }), async (req, res) => {
  console.log('Raw body length:', req.body.length);
  console.log('Raw body content:', req.body.toString());
  // 后续代码不变...
});

内容的提问来源于stack exchange,提问作者Mr Coder

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 23:34:50