如何在Stripe CLI触发的webhook POST请求中传递自定义变量
问题描述
需要实现自定义变量跨页面重载、跨第三方跳转后仍可正常传递调用,核心需求是读取req.session.variable的值,在Stripe CLI管控的router.post('/webhook', ...)接口中使用,当前存在两个问题:
- 用户跳转至Stripe等第三方站点后,
req.session内容无法在webhook POST请求中读取到 - Stripe webhook的POST接口要求
req.body必须为原始req.rawBody格式,常规JSON解析会导致签名校验失败
原有实现代码如下:
let express = require('express'); let router = express.Router(); let postMong = require('./post') require("dotenv").config() router.use(express.json()); const YOUR_DOMAIN = 'http://localhost:4242'; const stripe = require('stripe')(process.env.PUBLIC_KEY); const endpointSecret = 'whsec_1c7320d5c8878c54d6f99e0d5dcd441008c79b76ddb0a09727a4fd977d3ac1ed'; const fulfillOrder = (ses) => { console.log("Order Completed") } router.post('/checkout/create-order', async (req, res) => { const price = req.body.order.stripe_price || undefined, product = req.body.order.stripe_product || undefined const session = await stripe.checkout.sessions.create({ //shipping_address_collection: { // allowed_countries: ['US', 'CA'], //}, //shipping_options: [ // { // shipping_rate_data: { // type: 'fixed_amount', // fixed_amount: { // amount: 2499, // currency: 'usd', // }, // display_name: 'International Shipping', // // Delivers between 5-7 business days // delivery_estimate: { // minimum: { // unit: 'week', // value: 2, // }, // } // } // }, //], line_items: [ { price: price, quantity: 1, }, ], payment_method_types: ["card", 'us_bank_account'], mode: 'payment', success_url: `${YOUR_DOMAIN}/success.html`, cancel_url: `${YOUR_DOMAIN}/index.html`, }); res.json({url: session.url}) }); router.post('/webhook', (request, response) => { const payload = request.body; let username = request.session.username; // returns undefined because the session is empty after the redirect const sig = request.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(payload, sig, endpointSecret); } catch (err) { console.log(err.message) return response.status(400).send(`Webhook Error: ${err.message}`); } if (event.type === 'checkout.session.completed') { const session = event.data.object; fulfillOrder(session); } response.status(200); }); module.exports = router
解决方案
核心原理说明
首先明确两个认知误区:
- Stripe webhook请求是Stripe官方服务器主动向你的服务端发起的回调请求,发起方不是用户浏览器,不会携带用户本地存储的session cookie,因此无论是否发生跳转,你都不可能在webhook接口中读到
req.session的用户相关值,和跳转清空无关。 - Stripe webhook签名校验要求拿到原始未解析的请求body,全局挂载
express.json()会把请求体提前解析为JSON对象,导致签名校验失败。
具体实现步骤
- 拆分body解析逻辑:webhook路由单独使用
express.raw()处理原始请求体,其余业务路由正常使用express.json()解析JSON格式请求。 - 传递自定义变量不要依赖session:创建Stripe Checkout Session时,把需要传递的自定义字段全部存入Stripe自带的
metadata字段,webhook收到事件后直接从返回的Checkout Session对象中读取metadata即可,不需要额外存储,也不会丢失。
修改后可直接运行的代码
let express = require('express'); let router = express.Router(); let postMong = require('./post') require("dotenv").config() // 移除全局的express.json()挂载,按路由分别配置解析规则 const YOUR_DOMAIN = 'http://localhost:4242'; const stripe = require('stripe')(process.env.PUBLIC_KEY); const endpointSecret = 'whsec_1c7320d5c8878c54d6f99e0d5dcd441008c79b76ddb0a09727a4fd977d3ac1ed'; const fulfillOrder = (ses, username) => { // 这里可以直接拿到传入的username做后续业务处理 console.log("Order Completed, user:", username) } // 非webhook路由正常使用json解析 router.post('/checkout/create-order', express.json(), async (req, res) => { const price = req.body.order.stripe_price || undefined, product = req.body.order.stripe_product || undefined, // 从当前请求的session里拿到要传递的自定义变量 username = req.session.username; const session = await stripe.checkout.sessions.create({ line_items: [ { price: price, quantity: 1, }, ], payment_method_types: ["card", 'us_bank_account'], mode: 'payment', success_url: `${YOUR_DOMAIN}/success.html`, cancel_url: `${YOUR_DOMAIN}/index.html`, // 把自定义变量存在metadata里,Stripe会在后续webhook事件中原样返回 metadata: { username: username } }); res.json({url: session.url}) }); // webhook路由使用raw格式解析body,满足签名校验要求 router.post('/webhook', express.raw({type: 'application/json'}), (request, response) => { const payload = request.body; const sig = request.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(payload, sig, endpointSecret); } catch (err) { console.log(err.message) return response.status(400).send(`Webhook Error: ${err.message}`); } if (event.type === 'checkout.session.completed') { const session = event.data.object; // 直接从返回的session里读之前存入的metadata,拿到自定义变量 const username = session.metadata.username; fulfillOrder(session, username); } response.status(200).end(); }); module.exports = router
注:
metadata支持传入任意自定义键值对,只要值为字符串格式即可,单个Checkout Session的metadata总大小不超过500个键值对就可以正常使用,完全满足常规业务传值需求。
内容的提问来源于stack exchange,提问作者Cristian Babalau
相关产品推荐
相关产品推荐

