在支付系统开发中,最让开发者头疼的问题往往不是支付流程本身,而是如何处理用户的银行卡信息。直接存储卡号意味着要承担巨大的安全风险,还要面对严格的PCI DSS合规审计。Stripe的PaymentIntent API正是为了解决这个问题而设计的,它让敏感卡号数据全程不经过你的服务器,从架构层面化解了安全隐患。本文将结合实际代码,完整讲解PaymentIntent的创建、确认、异步通知处理以及安全设计要点。

PaymentIntent 的核心概念与令牌化设计
PaymentIntent是Stripe推荐的标准支付对象,它代表一次支付会话的完整生命周期,从创建、确认到最终成功或失败。它取代了早期直接用Token和Charge的组合方式,把支付状态管理交给Stripe,开发者只需关注业务逻辑。
令牌化机制是整个设计的灵魂。用户在前端输入卡号后,Stripe.js或Stripe Elements会在浏览器端把卡信息直接加密发送给Stripe服务器,返回一个不代表敏感信息的PaymentMethod ID,形如pm_xxxxxxxx。你的服务器从头到尾接触到的只有这个ID,真实的卡号、有效期和CVV永远不会到达你的数据库。这种方式在PCI DSS合规分级中属于最低负担的SAQ A级别,大多数情况下只需填写简单的自评问卷即可。
与一次性Token相比,PaymentMethod是可以复用的,配合Customer对象可以实现保存卡片、重复扣款等场景。同时PaymentIntent内置了状态机(requires_payment_method、requires_confirmation、processing、succeeded等),自动处理3D Secure验证、重试等复杂流程,这些都是旧的Charge API难以覆盖的。
服务端创建 PaymentIntent 完整流程
服务端的职责是创建PaymentIntent并返回其客户端密钥。以Node.js为例,首先安装官方SDK:
npm install stripe --save
接着在Express路由中编写创建逻辑。关键点包括金额单位是分、必须指定currency、自动支付方式automatic_payment_methods开启后Stripe会根据账户配置智能匹配支付渠道:
const stripe = require('stripe')('sk_test_你的密钥');
app.post('/create-payment-intent', async (req, res) => {
const { orderId, amount } = req.body;
try {
// 金额单位为最小货币单位,例如美元传的是美分
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: 'usd',
automatic_payment_methods: { enabled: true },
metadata: { order_id: orderId }, // 业务订单号,方便对账
});
// 只把client_secret返回给前端,绝不泄露sk密钥
res.json({ clientSecret: paymentIntent.client_secret });
} catch (err) {
res.status(500).json({ error: err.message });
}
});这里的client_secret是前端确认支付所需的凭证,它可以安全地传递给浏览器,因为它只能用于操作这一个PaymentIntent,无法发起其他支付。而sk_test_开头的私钥必须严格保管在服务端环境变量中,一旦泄露等于把收款权限拱手让人。金额建议由服务端根据订单数据计算,不要信任前端传来的数字,否则用户篡改请求就能以一分钱买走商品。
另外注意metadata字段的用法,把订单ID写入metadata后,后续Webhook回调时可以直接读取,实现支付结果与业务订单的精准绑定,避免依赖金额或时间去模糊匹配。
前端确认支付与卡信息安全处理
前端推荐使用Stripe.js配合Elements组件,它提供了符合PCI规范的输入控件。引入脚本并初始化:
<script src="https://js.stripe.com/v3/"></script> <form id="payment-form"> <div id="card-element"><!-- 卡信息输入框由Stripe渲染 --></div> <button id="submit">支付</button> </form>
const stripe = Stripe('pk_test_你的公钥');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
document.getElementById('payment-form').addEventListener('submit', async (e) => {
e.preventDefault();
// 先调用后端接口拿到clientSecret
const { clientSecret } = await fetch('/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId: 1024, amount: 999 }),
}).then(r => r.json());
// 确认支付,卡数据在此过程中直接传给Stripe,不经过你的服务器
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: 'https://ipipp.com/payment/result' },
});
if (error) {
alert(error.message);
}
});Elements渲染的输入框运行在Stripe托管的iframe中,你的页面脚本无法读取其中的卡号明文,这是合规设计的技术保障。即便开发者想违规收集卡号,在默认架构下也做不到。若银行要求3D Secure验证,Stripe会自动跳转或弹出验证界面,验证完成后回到return_url,前端再调用retrievePaymentIntent获取最终状态。
千万不要为了图省事用自定义表单收集卡号再通过接口发给Stripe,那样会让你的系统落入更高的PCI合规等级,审计成本和泄露风险都会成倍增加。
Webhook 异步通知与订单状态更新
支付的最终结果必须以Webhook为准,而不是前端回调。用户可能关闭浏览器、网络可能中断,只有Stripe服务器主动推送的payment_intent.succeeded事件才是可靠的发货依据。
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
// 验证签名,防止伪造请求
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'payment_intent.succeeded') {
const intent = event.data.object;
const orderId = intent.metadata.order_id;
// 根据orderId将业务订单标记为已支付,幂等处理很重要
console.log(`订单 ${orderId} 支付成功,金额 ${intent.amount}`);
}
res.json({ received: true });
});Webhook处理有两个必须注意的细节。第一是签名验证,constructEvent会校验请求是否真的来自Stripe,跳过这一步等于给攻击者敞开了免费下单的大门。第二是幂等性,Stripe可能对同一事件重复推送,处理逻辑要先检查订单是否已更新过状态,或利用数据库唯一约束防止重复发货。
建议对处理过的Webhook事件ID做持久化记录,收到重复事件时直接返回成功不再执行业务逻辑,这是支付系统健壮性的基本功。
常见安全误区与最佳实践总结
梳理几个高频踩坑点:其一,把私钥硬编码在前端代码或提交到代码仓库,正确做法是使用环境变量管理;其二,信任前端金额,金额必须服务端计算并创建;其三,只依赖前端跳转结果页判断支付成功,务必以Webhook为准;其四,在自家数据库存卡号,即使加密存储也是高风险行为,应使用PaymentMethod配合Customer实现复用卡场景。
对于需要保存卡片供下次使用的场景,正确的姿势是先创建Customer对象,把payment_methodattach到该客户,之后通过指定off_session参数即可在用户不在线时扣款,全程同样无需接触卡号明文。退款操作也很简单,调用stripe.refunds.create传入PaymentIntent ID即可,资金会原路退回。
整体来看,Stripe PaymentIntent这套架构的核心思想是责任转移:把最危险的敏感数据交给专业的基础设施处理,开发者专注业务层。只要遵循令牌化、服务端控金额、Webhook定结果这三条原则,就能以最低的合规成本搭建出一套安全可靠的支付系统。
Stripe PaymentIntent支付卡安全PCI合规修改时间:2026-09-08 00:50:37