Node环境下Stripe Webhook签名验证失败及元数据传递问题
1. 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
怀疑使用了错误的whsec密钥:Stripe示例提供的whsec与测试Webhook时显示的签名密钥不一致。
2. Session元数据无法传递到Webhook
创建Checkout Session时已设置包含用户ID的metadata,打印Session能看到该数据,但Webhook事件中无法获取,需修复以通过元数据更新付费用户信息。
已尝试用req.body和req.rawBody处理请求,问题仍未解决。
index.js
const functions = require('firebase-functions'); const admin = require('firebase-admin'); const express = require('express'); const serviceAccount = require("./credentials.json"); const cors = require('cors'); const jwt = require('jsonwebtoken'); const dotenv = require('dotenv'); const cookieParser = require('cookie-parser'); dotenv.config(); const app = express(); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: process.env.URL }); app.use(cookieParser(process.env.TOKEN_SECRET)); app.use(cors({ origin: true, credentials: true })); app.use('/api', require('./routes/stripe.webhook.routes')); // Webhook routes app.use('/api', express.json()); app.use('/api', require('./routes/login.routes')); app.use('/api', require('./routes/register.routes')); app.use('/api/', require('./routes/auth.routes')); app.use('/api', require('./routes/schedule.routes')); app.use('/api', require('./routes/appointments.routes')); app.use('/api', require('./routes/users.routes')); app.use('/api', require('./routes/doctors.routes')); app.use('/api', require('./routes/illness.routes')); app.use('/api', require('./routes/stripe.routes')); exports.app = functions.https.onRequest(app);
stripe.webhook.routes.js
const express = require('express'); const router = express.Router(); const dotenv = require('dotenv'); dotenv.config(); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const admin = require('firebase-admin'); const endpointSecret = process.env.WHSEC; router.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { const sig = req.headers['stripe-signature']; let event; try { // Verificar que el webhook venga de Stripe usando la firma event = stripe.webhooks.constructEvent( req.rawBody, sig, endpointSecret, ); } catch (err) { console.error(`Webhook signature verification failed.`, err.message); console.log(err) return res.status(400).send(`Webhook Error: ${err}`); } switch (event.type) { case 'invoice.payment_succeeded': console.log('Succeed') //handlePaymentSucceeded(event.data.object); break; case 'invoice.payment_failed': console.log('Failed') break; default: console.log(`Unhandled event type ${event.type}`); } res.status(200).send(req); }); module.exports = router;
stripe.routes.js
// IMPORTS - - - - - - - - - - - - - - - - - - - - - - - - const admin = require('firebase-admin') const {Router} = require('express') const dotenv = require('dotenv'); // CONFIG - - - - - - - - - - - - - - - - - - - - - - - - const router = Router(); const db = admin.firestore(); dotenv.config(); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY) router.post('/create-checkout-session', async (req, res) => { try { const userId = req.body.userId; createSession({priceId: req.body.priceId, userId: userId}).then(response => { return res.status(200).send(response); }).catch(e => { return res.status(400).send(e); }); } catch (e) { } }); async function createSession(params) { console.log(params) try { const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [{price: params.priceId, quantity: 1}], mode: 'subscription', success_url: process.env,SUCCESS, cancel_url: process.env.FAILED, metadata: { userId: params.userId, test: 'test } }); console.log('Session created: ', session); return { id: session.id }; } catch (error) { console.log(error) throw new Error(`Failed to create session: ${error.message}`); } } module.exports = router
一、修复Webhook签名验证问题
确认whsec密钥正确性
- 登录Stripe后台,进入Developers > Webhooks页面
- 找到目标Webhook端点,点击Reveal signing secret,复制该值到
.env文件的WHSEC变量中,不要使用示例中的测试密钥。
确保原始请求体未被修改
- 当前
index.js中已将Webhook路由放在express.json()中间件之前,这是正确的,避免请求体被JSON解析修改。需确认没有其他中间件(如cors、cookieParser)会修改请求体内容。
- 当前
适配Firebase Functions的请求体处理
Firebase Functions转发请求时可能会处理请求体,可尝试将req.rawBody替换为req.body(express.raw会把原始数据存入req.body):event = stripe.webhooks.constructEvent( req.body, sig, endpointSecret, );
二、修复Session元数据传递到Webhook的问题
理解订阅模式下的元数据传递逻辑
订阅模式中,Checkout Session的元数据不会直接传递到invoice.payment_succeeded事件,需将元数据附加到订阅对象上,或通过订阅ID反向获取元数据。修改Session创建代码,附加元数据到订阅
在createSession函数中添加subscription_data.metadata字段,同时修复两处语法错误:async function createSession(params) { console.log(params) try { const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [{price: params.priceId, quantity: 1}], mode: 'subscription', success_url: process.env.SUCCESS, // 修复拼写错误:原代码为process.env,SUCCESS cancel_url: process.env.FAILED, metadata: { userId: params.userId, test: 'test' // 修复引号未闭合问题 }, subscription_data: { metadata: { userId: params.userId, test: 'test' } } }); console.log('Session created: ', session); return { id: session.id }; } catch (error) { console.log(error) throw new Error(`Failed to create session: ${error.message}`); } }在Webhook中获取元数据并更新用户信息
修改invoice.payment_succeeded事件的处理逻辑:case 'invoice.payment_succeeded': console.log('Succeed'); const invoice = event.data.object; const subscriptionId = invoice.subscription; // 获取订阅对象及元数据 const subscription = await stripe.subscriptions.retrieve(subscriptionId); const userId = subscription.metadata.userId; console.log('User ID from metadata:', userId); // 更新Firebase用户订阅状态 await db.collection('users').doc(userId).update({ isSubscribed: true, subscriptionId: subscriptionId }); break;
内容的提问来源于stack exchange,提问作者Lopez Zubia Pablo Daniel

