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

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签名验证问题

  1. 确认whsec密钥正确性

    • 登录Stripe后台,进入Developers > Webhooks页面
    • 找到目标Webhook端点,点击Reveal signing secret,复制该值到.env文件的WHSEC变量中,不要使用示例中的测试密钥。
  2. 确保原始请求体未被修改

    • 当前index.js中已将Webhook路由放在express.json()中间件之前,这是正确的,避免请求体被JSON解析修改。需确认没有其他中间件(如cors、cookieParser)会修改请求体内容。
  3. 适配Firebase Functions的请求体处理
    Firebase Functions转发请求时可能会处理请求体,可尝试将req.rawBody替换为req.body(express.raw会把原始数据存入req.body):

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

二、修复Session元数据传递到Webhook的问题

  1. 理解订阅模式下的元数据传递逻辑
    订阅模式中,Checkout Session的元数据不会直接传递到invoice.payment_succeeded事件,需将元数据附加到订阅对象上,或通过订阅ID反向获取元数据。

  2. 修改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}`);
      }
    }
    
  3. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 15:36:17