如何通过 JavaScript 和 Payment Request API 安全处理付款?
结论
Payment Request API 不是支付处理商,也不会自动扣款。它的作用是调用浏览器原生支付界面,再把用户所选支付方式的结果交给网站。授权、扣款、3-D Secure 验证、退款和风控仍需由服务端通过支付服务商完成。
用 JavaScript 获取 PAN、有效期和 CVC,再发送到自己的服务器,并非绝对无法满足 PCI DSS,但这样通常会让网站和服务器进入更严格的合规范围,带来较高的成本和风险。使用 Payment Request API 并不会让这条数据链路自动合规。
实际项目中,更合适的做法是使用支付服务商提供的 JavaScript SDK 或数字钱包,让浏览器返回一次性 token 或 PaymentMethod 标识,避免业务系统接触原始卡号。例如,Stripe 曾通过 Stripe.js 的 Payment Request Button 集成 Payment Request API,并把支付结果转换为 PaymentIntents 可用的 PaymentMethod。服务商可能调整具体产品入口,因此实施前需要核对最新文档和浏览器支持范围。
Payment Request API 在流程中的位置
一笔完整的支付大致经过以下流程:
浏览器展示支付界面
↓
Payment Request API 返回支付凭据或钱包令牌
↓
前端把令牌发送给业务服务器
↓
业务服务器调用支付处理商 API
↓
支付处理商完成授权、3-D Secure 和扣款
↓
服务器根据 Webhook 确认最终支付状态
Payment Request API 的 response.complete() 只会通知浏览器关闭支付界面,并显示成功或失败状态,并不表示银行卡已经扣款成功。调用它之前,至少要取得支付处理商的初步确认。订单的最终状态应以服务端查询结果或通过签名验证的 Webhook 为准。
为什么会涉及 PCI DSS
是否涉及 PCI DSS,取决于原始银行卡数据经过哪些系统,而不是页面由客户端还是服务端生成。
如果页面获取卡号、有效期和安全码,并把这些信息发送到自己的服务器,那么:
- 服务器、日志、监控系统和相关网络环境都可能被纳入 PCI DSS 评估范围;
- 卡号和 CVC 不能写入日志、分析系统、错误报告或数据库;
- 完成授权后不得保存 CVC;
- 系统需要满足访问控制、漏洞管理、密钥管理和安全测试等要求;
- 数据来自浏览器原生界面,并不意味着商户只需填写最简化的 SAQ。
使用支付服务商托管的字段、官方 SDK 或钱包令牌化方案时,原始卡数据通常会直接进入支付服务商的受控环境,商户系统只接触 token。这通常可以缩小 PCI DSS 的适用范围,但不等于完全无需合规。具体适用哪种 SAQ、是否需要其他验证,应由收单机构、支付服务商或合格的 PCI 顾问根据实际数据流确定。
推荐的实现方式
1. 不要依赖 basic-card
早期示例经常使用:
{
supportedMethods: "basic-card"
}
这种方式要求商户直接处理银行卡明细,而且浏览器支持已经不可靠。Payment Request API 框架仍可用于钱包或特定支付处理程序,但不适合作为新的跨浏览器信用卡方案。
2. 通过支付服务商 SDK 创建令牌
下面的示例展示了 Stripe.js Payment Request 集成的典型结构,主要用于说明正确的数据流。Stripe 的推荐组件、可用支付方式和接口细节可能发生变化,上线时应以当前官方文档为准。
const stripe = Stripe("pk_test_your_publishable_key");
const paymentRequest = stripe.paymentRequest({
country: "US",
currency: "usd",
total: {
label: "Order total",
amount: 2999
},
requestPayerName: true,
requestPayerEmail: true
});
paymentRequest.on("paymentmethod", async (event) => {
try {
const response = await fetch("/api/payments/confirm", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
paymentMethodId: event.paymentMethod.id,
orderId: "order_123"
})
});
const result = await response.json();
if (!response.ok) {
event.complete("fail");
return;
}
event.complete("success");
if (result.requiresAction) {
const confirmation = await stripe.confirmCardPayment(
result.clientSecret
);
if (confirmation.error) {
// 显示验证失败信息,但不要记录卡片敏感数据
}
}
} catch (error) {
event.complete("fail");
}
});
前端传给服务器的是 paymentMethodId,不是卡号和 CVC。最终金额也不能由前端决定,服务器必须根据可信的订单数据重新计算。
服务端可以创建并确认 PaymentIntent:
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.post("/api/payments/confirm", async (req, res) => {
const order = await loadOrder(req.body.orderId);
// 应在这里验证登录用户、订单状态、库存、币种和真实金额
const paymentIntent = await stripe.paymentIntents.create({
amount: order.amount,
currency: order.currency,
payment_method: req.body.paymentMethodId,
confirm: true,
automatic_payment_methods: {
enabled: true
},
return_url: "https://example.com/payment/return",
metadata: {
orderId: order.id
}
});
res.json({
paymentIntentId: paymentIntent.id,
clientSecret: paymentIntent.client_secret,
requiresAction:
paymentIntent.status === "requires_action"
});
});
生产环境还需要处理幂等键、重复提交、异常恢复和支付服务商 Webhook。不能只根据前端返回的“成功”状态发货。
可以选择哪些支付处理商
Stripe 是一个明确的选择。Stripe.js 可以对支付凭据进行令牌化,服务端再通过 PaymentIntents 完成支付,从而避免商户应用直接传输原始卡号。
Adyen、Braintree 等服务商也提供钱包、托管组件或浏览器支付集成,但它们未必允许商户把 Payment Request API 返回的任意原始卡数据直接提交给普通商户接口。多数服务商要求使用自己的 SDK、组件或约定的支付方法标识,以保证令牌化、3-D Secure 和合规数据流正常工作。
选择支付服务商时,需要确认:
- 是否明确支持目标浏览器和目标钱包;
- 银行卡数据是否由服务商直接接收并令牌化;
- 是否支持当地要求的 3-D Secure 或强客户认证;
- 商户系统适用的 PCI DSS 范围和 SAQ 类型;
- 是否提供服务端确认、幂等处理和签名 Webhook;
- 当用户的浏览器不支持 Payment Request API 时,是否有可靠的回退结账页面。
合适的做法不是寻找能够接收任意原始 PaymentResponse 的处理商,而是选择提供 Payment Request、数字钱包或同类快速结账组件的支付服务商,并按照其要求使用令牌化流程。这样既能完成实际扣款,也能尽量把银行卡敏感数据隔离在支付服务商的 PCI DSS 环境中。
备注:内容仅供参考。
