服务器端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
相关产品推荐
相关产品推荐

