PAYATHON 2026

不存储信用卡信息,如何实现结账时自动填充?

支付老李

结论

不要在自己的数据库中保存卡号、有效期或安全码。应使用符合 PCI DSS 要求的支付服务商,并采用”卡片保险库(Vault)+ 令牌化(Tokenization)“方案。

用户首次输入银行卡信息时,由支付服务商的托管页面、Hosted Fields 或官方前端 SDK 直接采集数据。真实卡片信息保存在支付服务商处,服务商会返回一个不可逆的支付方式标识,例如 payment_method_id。你的系统只需保存:

  • 支付服务商的客户 ID
  • 支付方式令牌或 ID
  • 卡品牌、尾号、有效期等展示信息
  • 是否为默认支付方式

用户再次结账时,后端通过已保存的支付方式 ID 发起支付,无需重新输入完整卡号。

为什么普通加密不能替代令牌化

即使先加密再写入数据库,卡号仍属于系统保存的信用卡信息。只要系统可以解密,密钥、应用服务器、数据库备份、日志和运维流程就可能成为攻击目标,PCI DSS 合规范围也会明显扩大。

还要注意以下几点:

  • CVV/CVC 属于敏感认证数据,授权完成后不得保存,即使加密也不行。
  • 浏览器 autocomplete 使用的是用户设备或密码管理器保存的信息,不能保证跨设备、跨浏览器使用。
  • 不要把卡号写入 Cookie、localStorage、Session、日志或分析系统。
  • 令牌并不是加密后的卡号。它通常只能用于指定的支付服务商、商户账户或使用场景。

大型电商平台可能会自行建设符合严格安全标准的卡片保险库,也可能采用支付服务商或银行卡网络提供的令牌化能力。普通网站通常没有必要承担这套基础设施带来的成本和风险。

推荐的实现流程

1. 选择支持保存卡片的支付服务商

至少确认服务商支持以下功能:

  • 托管结账页或 Hosted Fields
  • 卡片令牌化
  • Customer 与 Payment Method 绑定
  • 使用已保存的支付方式扣款
  • 3-D Secure、SCA 等额外认证
  • Webhook 签名验证
  • 支付方式更新、删除和过期处理

如果想尽量缩小 PCI DSS 范围,优先选择由服务商托管输入框或整个结账页面的集成方式。究竟适用 SAQ A、SAQ A-EP 还是其他合规范围,要根据页面的实际集成方式和收单机构的要求确认,不能只凭”没有保存卡号”来判断。

2. 首次结账时直接令牌化

页面中的卡片输入框应由支付服务商的 SDK 创建。真实卡号直接发送给支付服务商,不经过你的应用服务器。

概念示例:

<form id="payment-form">
  <div id="provider-card-field"></div>

  <label>
    <input id="save-card" type="checkbox">
    保存此支付方式
  </label>

  <button type="submit">支付</button>
</form>
const paymentProvider = PaymentProvider.initialize(PUBLIC_KEY);
const cardField = paymentProvider.createCardField();

cardField.mount("#provider-card-field");

document
  .querySelector("#payment-form")
  .addEventListener("submit", async (event) => {
    event.preventDefault();

    const result = await paymentProvider.createPaymentMethod({
      card: cardField
    });

    if (result.error) {
      showError(result.error.message);
      return;
    }

    await fetch("/api/checkout", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": getCsrfToken()
      },
      body: JSON.stringify({
        paymentMethodId: result.paymentMethod.id,
        saveForFuture: document.querySelector("#save-card").checked
      })
    });
  });

PaymentProvider、createCardField() 和示例中的返回字段只是用于说明流程的抽象名称,并非某家服务商的真实 API。实际开发时,必须使用所选服务商的官方 SDK 和文档。

前端只应把 paymentMethodId 发给自己的后端,不能发送卡号或 CVV。

3. 后端绑定支付方式

后端先验证当前登录用户,再到支付服务商处创建或查找 Customer,并把支付方式绑定到该 Customer。

app.post("/api/checkout", requireLogin, async (req, res) => {
  const { paymentMethodId, saveForFuture } = req.body;

  const customerId = await getOrCreateProviderCustomer(req.user.id);

  const payment = await paymentProvider.payments.create({
    customerId,
    paymentMethodId,
    amount: 12900,
    currency: "CNY",
    saveForFuture
  }, {
    idempotencyKey: req.headers["idempotency-key"]
  });

  if (saveForFuture) {
    await db.savedPaymentMethods.upsert({
      userId: req.user.id,
      provider: "selected-provider",
      providerCustomerId: customerId,
      providerPaymentMethodId: payment.paymentMethod.id,
      brand: payment.paymentMethod.card.brand,
      last4: payment.paymentMethod.card.last4,
      expiryMonth: payment.paymentMethod.card.expiryMonth,
      expiryYear: payment.paymentMethod.card.expiryYear
    });
  }

  res.json({
    paymentId: payment.id,
    status: payment.status,
    clientAction: payment.clientAction
  });
});

金额必须由后端根据商品、优惠和订单数据重新计算,不能采用浏览器提交的金额。

数据库可以保存类似下面的记录:

user_id
provider
provider_customer_id
provider_payment_method_id
brand
last4
expiry_month
expiry_year
is_default
created_at

以下数据不能保存:

完整卡号 PAN
CVV/CVC
磁道数据
PIN 或 PIN block
前端提交的原始卡片表单
可还原完整卡号的调试日志

4. 再次结账时显示已保存的卡片

用户再次进入结账页时,可以向其显示非敏感的卡片摘要:

Visa •••• 4242
有效期 08/2029

用户选中卡片后,浏览器只提交该支付方式在本系统中的记录 ID:

{
  "savedPaymentMethodRecordId": "spm_8f21..."
}

后端必须确认这条记录属于当前登录用户,然后再取出真正的 providerPaymentMethodId:

app.post("/api/orders/:orderId/pay", requireLogin, async (req, res) => {
  const savedMethod = await db.savedPaymentMethods.findOne({
    id: req.body.savedPaymentMethodRecordId,
    userId: req.user.id
  });

  if (!savedMethod) {
    return res.status(404).json({ error: "Payment method not found" });
  }

  const order = await loadPayableOrder(req.params.orderId, req.user.id);

  const payment = await paymentProvider.payments.create({
    customerId: savedMethod.providerCustomerId,
    paymentMethodId: savedMethod.providerPaymentMethodId,
    amount: order.amount,
    currency: order.currency,
    orderReference: order.id
  }, {
    idempotencyKey: `pay:${order.id}`
  });

  res.json({
    status: payment.status,
    clientAction: payment.clientAction
  });
});

不要接受客户端直接传入的任意支付服务商令牌,否则攻击者可能尝试引用不属于自己的支付方式。

自动填充和保存卡片不是一回事

如果只是想减少用户输入,可以正确设置 HTML 自动填充属性:

<input
  name="cardholder"
  autocomplete="cc-name"
  inputmode="text"
>

<input
  name="card-number"
  autocomplete="cc-number"
  inputmode="numeric"
>

<input
  name="expiry"
  autocomplete="cc-exp"
  inputmode="numeric"
>

<input
  name="security-code"
  autocomplete="cc-csc"
  inputmode="numeric"
>

这样,浏览器或用户选择的密码管理器可以自动填写卡片信息,网站自身不会保存这些数据。但自动填充依赖具体设备和浏览器,无法提供稳定的账户级”已保存卡片”体验。

如果用户需要登录后在不同设备上选择同一张卡,应使用支付服务商提供的令牌化保存方案。

必须处理的安全细节

用户授权

保存卡片前,应取得用户的明确同意,并说明用途,例如”保存此支付方式,供以后购买使用”。如果以后要在用户不在场时自动续费或扣款,还要按照支付服务商和当地规则记录相应授权。一次性结账同意不能直接视为无限期扣款授权。

身份验证

查看、添加、删除或使用已保存的支付方式时,必须要求用户登录。修改默认卡、使用异常设备或支付高金额订单时,可以要求用户重新验证密码或进行多因素认证。

额外验证

保存卡片不代表以后一定可以在没有交互的情况下扣款。发卡行、地区法规或风控策略可能要求用户完成 3-D Secure、SCA、短信验证或其他验证。接口要能返回”需要用户操作”的状态,并让前端继续完成认证流程。

Webhook

不能只根据浏览器的跳转结果把订单标记为已付款。应验证支付服务商 Webhook 的签名,再根据服务商确认的最终状态更新订单。重复通知、乱序通知、退款和支付失败也要妥善处理。

幂等性

创建支付时应使用幂等键,防止用户重复点击、网络重试或服务超时导致重复扣款。通常可以用订单 ID 和支付尝试序号生成稳定的幂等键。

日志与监控

不要记录包含卡片数据的完整请求体,还应检查以下位置:

  • 反向代理访问日志
  • APM 和错误追踪
  • 前端埋点与会话回放
  • 客服工单
  • 数据库备份
  • 崩溃报告
  • 测试环境和截图

即使使用 Hosted Fields,也不能让第三方分析脚本读取或覆盖支付组件周围的敏感交互。

删除与失效

用户删除卡片时,应同时在支付服务商处解绑或删除相应支付方式,并清理本地映射。系统还要处理卡片过期、支付方式被服务商撤销、Customer 被删除和商户账户迁移等情况。

最实际的架构选择

对大多数网站来说,较为安全且容易维护的方案是:

  1. 由支付服务商采集并保存真实卡片数据。
  2. 网站只保存 Customer ID、Payment Method ID、卡片尾号等展示信息。
  3. 后续支付由后端引用这些标识发起。
  4. 需要额外认证时,由服务商 SDK 接管相关流程。
  5. 通过签名 Webhook 确认最终支付结果。
  6. 根据实际集成方式完成 PCI DSS 自评,并满足收单机构的要求。

这种方案可以提供接近 Amazon 的结账体验,又不必把自己的数据库变成信用卡保险库。令牌化可以降低风险并缩小合规范围,但不能免除 HTTPS、访问控制、密钥管理、漏洞修复、日志治理和 PCI DSS 责任。

备注:内容仅供参考。