如何在不离开网站的情况下接受信用卡付款?
结论
顾客可以留在当前网站完成信用卡付款,但你不应自行收集、存储或处理完整卡号。通常可以选择支付服务商提供的以下方案:
- 嵌入式支付表单(Hosted Fields / Payment Element)
- 由支付服务商托管并通过
iframe嵌入的结账组件 - 前端令牌化(Tokenization),后端只接收支付令牌并创建付款
采用这些方案后,页面仍可使用你的域名和设计,但卡号、有效期和安全码会直接提交给支付服务商,不经过你的服务器。
部分交易触发 3-D Secure、银行验证或钱包授权时,仍可能出现弹窗、内嵌验证窗口,甚至短暂跳转。具体行为取决于发卡行、所在地区和支付服务商,无法保证所有付款都不出现外部验证界面。
推荐的支付流程
较安全的典型流程是:
- 顾客在你的网站确认商品和金额。
- 你的服务器根据订单创建支付会话或支付意图。
- 浏览器加载支付服务商提供的 JavaScript SDK。
- SDK 在当前页面渲染由服务商托管的信用卡输入框。
- 顾客填写卡片信息。
- 浏览器将卡片信息直接发送给支付服务商,并转换为令牌。
- 你的服务器使用令牌确认付款。
- 支付服务商通过 Webhook 通知最终付款结果。
- 你的服务器验证通知,并将订单标记为已付款。
订单金额必须由服务器根据商品、数量、折扣和运费重新计算,不能信任浏览器提交的金额。
前端示例
各家支付服务商使用的 SDK 和方法名不同。下面是一个通用结构示例,其中的 PaymentProvider、接口路径和事件名称需要替换为所选服务商的实际实现:
<form id="payment-form">
<div id="card-element"></div>
<p id="payment-error" role="alert"></p>
<button id="pay-button" type="submit">立即付款</button>
</form>
<script src="https://js.payment-provider.example/sdk.js"></script>
<script>
const provider = PaymentProvider({
publicKey: "pk_public_key"
});
const cardElement = provider.createCardElement();
cardElement.mount("#card-element");
const form = document.querySelector("#payment-form");
const button = document.querySelector("#pay-button");
const errorElement = document.querySelector("#payment-error");
form.addEventListener("submit", async (event) => {
event.preventDefault();
button.disabled = true;
errorElement.textContent = "";
try {
const sessionResponse = await fetch("/api/payments/session", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
orderId: "ORDER_ID"
})
});
if (!sessionResponse.ok) {
throw new Error("无法创建支付会话");
}
const { clientSecret } = await sessionResponse.json();
const result = await provider.confirmCardPayment(clientSecret, {
paymentMethod: {
card: cardElement
}
});
if (result.error) {
throw new Error(result.error.message || "付款失败");
}
// 此处只能显示“正在确认”。
// 订单是否付款成功,应以后端查询结果或 Webhook 为准。
window.location.href = `/orders/ORDER_ID/status`;
} catch (error) {
errorElement.textContent = error.message;
button.disabled = false;
}
});
</script>
信用卡输入框即使看起来属于你的页面,通常也是支付服务商生成的独立 iframe,因此卡片信息不会进入你的 DOM、应用日志或后端接口。
后端示例
下面的 Node.js 示例说明了前后端的基本职责划分。支付服务商的方法名仅作为占位:
import express from "express";
import PaymentSDK from "payment-provider-sdk";
const app = express();
const payment = new PaymentSDK(process.env.PAYMENT_SECRET_KEY);
app.use(express.json());
app.post("/api/payments/session", async (req, res) => {
const { orderId } = req.body;
// 必须从数据库读取订单,不能采用前端提供的金额。
const order = await findOrderById(orderId);
if (!order || order.status !== "pending") {
return res.status(400).json({
error: "订单不存在或状态不允许付款"
});
}
const amount = calculateOrderAmount(order);
const session = await payment.createPayment({
amount,
currency: order.currency,
metadata: {
orderId: order.id
},
idempotencyKey: `pay:${order.id}`
});
res.json({
clientSecret: session.clientSecret
});
});
私钥只能保存在服务器环境变量或密钥管理系统中,不能写入网页、移动端代码或公开仓库。前端只能使用服务商明确标注为可以公开的公钥。
使用 Webhook 确认付款
浏览器显示付款成功,并不表示订单一定已经结算。顾客可能关闭页面,网络响应也可能丢失,所以应以服务商发送的服务器通知或后端主动查询的结果为准。
app.post(
"/webhooks/payment",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = payment.verifyWebhook({
payload: req.body,
signature: req.headers["payment-signature"],
secret: process.env.PAYMENT_WEBHOOK_SECRET
});
} catch {
return res.sendStatus(400);
}
if (event.type === "payment.succeeded") {
const orderId = event.data.metadata.orderId;
// 更新操作应可重复执行,避免重复通知导致重复发货。
await markOrderPaidOnce(orderId, event.data.paymentId);
}
res.sendStatus(200);
}
);
实际项目中必须按照支付服务商的文档保留 Webhook 原始请求体,并使用签名密钥验证来源。不要只凭请求中的 orderId 或 status 更新订单。
不建议直接制作信用卡输入框
如果使用普通 <input> 收集卡号并提交到你的服务器,系统就会直接接触持卡人数据。这通常会明显扩大 PCI DSS 合规范围,并带来以下风险:
- 卡号进入访问日志、错误日志或分析系统
- 调试工具、浏览器扩展或第三方脚本读取表单
- 数据库备份意外保存敏感信息
- 应用漏洞导致信用卡信息泄露
- 安全审计和合规成本大幅增加
尤其不要存储 CVV/CVC。即使自行加密了卡号,也不代表已经满足支付行业的安全和合规要求。
选择支付服务商时要确认什么
评估支付方案时,可以直接查找以下关键词:
Hosted FieldsEmbedded CheckoutPayment ElementTokenizationJavaScript SDK3-D SecureWebhookPCI DSS
还要确认服务商是否支持你的注册国家或地区、结算币种、退款、拒付处理、订阅付款以及当地法规要求。
嵌入式组件通常可以缩小 PCI DSS 合规范围,但能否适用某种特定的自我评估问卷,例如 SAQ A,取决于支付集成方式、页面中的第三方脚本以及服务商的具体要求。不能仅凭使用了 iframe 就自行认定。
上线前的关键检查
- 整个结账页面强制使用 HTTPS。
- 卡号和 CVV 不经过你的服务器。
- 私钥只保存在后端。
- 后端独立计算订单金额和币种。
- 验证 Webhook 签名。
- 使用幂等键防止重复扣款。
- 防止同一订单被重复发货或重复入账。
- 不在日志中记录支付令牌、卡片数据或完整请求体。
- 正确处理付款处理中、失败、取消、退款和拒付状态。
- 使用服务商提供的测试环境和测试卡完成异常流程测试。
要实现“不离开网站付款”,正确做法是在当前页面嵌入支付服务商托管的输入组件,再由后端配合令牌化、支付会话和 Webhook 完成交易。这样既能让结账过程保持连贯,也能避免应用直接处理最敏感的卡片数据。
备注:内容仅供参考。
