PAYATHON 2026

如何安全获取并存储 HTTPS 请求所需的信用卡信息

支付老李

结论

不要在 App、Keychain/Keystore、数据库或自有服务器中保存完整卡号、有效期和 CVV。应使用支付服务商提供的移动端 SDK、托管支付页面或 Hosted Fields,让信用卡信息直接进入其 PCI DSS 合规环境,并换取不可逆的支付令牌(token/payment method ID)。

后续付款只需保存并复用令牌:

信用卡信息 → 支付服务商 → 支付令牌 → POS API 完成扣款

如果第三方 API 只接受原始信用卡信息,又不提供令牌化、支付保险库(vault)或官方客户端 SDK,就不适合用于“保存卡片”和重复付款。此时需要第三方升级接口,或改用支持令牌化的支付处理方。自行加密卡号、将其放进 Keychain 或保存到服务器,都无法解决这个问题。

为什么只有 HTTPS 还不够

HTTPS 可以防止数据在网络传输过程中被轻易窃听,但无法解决这些问题:

  • App 是否会把卡号写入日志、崩溃报告、剪贴板或分析平台
  • 完整卡号是否会进入自有服务器、消息队列和备份
  • API 密钥是否被硬编码在 App 中
  • 设备被攻破后,内存或本地存储中的数据是否可能泄露
  • 保存信用卡数据带来的 PCI DSS 合规责任
  • 第三方是否有资格处理和保存持卡人数据

还要特别注意,CVV/CVC 属于敏感认证数据,完成授权后不得保存,即使经过加密也不能保存。重复付款不能依靠保存 CVV 实现。

推荐流程

1. 由后端创建一次性支付会话

App 先向自己的后端申请一个有效期短、权限受限的支付会话。长期 API 密钥只能保存在后端,不能打包进 App。

App → 自有后端:创建支付会话
自有后端 → 支付服务商:使用服务端凭证创建会话
自有后端 → App:返回短期 client token/session token

后端只处理订单号、用户编号和金额等业务数据,不接收完整卡号或 CVV。

2. 使用支付服务商的 UI 或 SDK 收集卡片

通常可按以下顺序选择:

  1. 支付服务商的移动端 SDK
  2. 托管支付页面
  3. Web 场景中的 Hosted Fields
  4. Apple Pay、Google Pay 等钱包方案

这些组件会收集卡号,并将数据直接发送给支付服务商。不要让信用卡字段经过自己的通用表单、埋点系统或日志框架。

下面的代码仅作示意。类型和方法名都是占位符,必须替换成实际服务商 SDK 提供的接口:

let cardForm = PaymentProviderCardForm()

cardForm.onTokenized = { result in
    let paymentMethodId = result.paymentMethodId

    submitOrder(
        orderId: currentOrder.id,
        paymentMethodId: paymentMethodId
    )
}

cardForm.onFailure = { error in
    showPaymentError(error.userSafeMessage)
}

cardForm.present(using: shortLivedClientToken)

重点不在具体语言,而在于信用卡数据由 SDK 直接令牌化。业务代码最终只能拿到 paymentMethodId,不应接触完整卡号和 CVV。

3. 使用令牌完成付款

App 将订单信息和支付令牌发送给自己的后端。后端检查用户身份、订单金额和订单状态后,再调用第三方支付或 POS API。

POST /api/orders/order_123/pay
Authorization: Bearer <user-session-token>
Content-Type: application/json
Idempotency-Key: 7e51c8c8-...

{
  "paymentMethodId": "pm_opaque_token"
}

服务端示意:

app.post("/api/orders/:orderId/pay", authenticateUser, async (req, res) => {
  const order = await loadOrderForUser(
    req.params.orderId,
    req.user.id
  );

  if (!order || order.status !== "pending") {
    return res.status(409).json({ error: "Order cannot be paid" });
  }

  const result = await paymentProvider.charge({
    amount: order.amount,
    currency: order.currency,
    paymentMethodId: req.body.paymentMethodId,
    idempotencyKey: req.get("Idempotency-Key"),
    metadata: {
      orderId: order.id
    }
  });

  await recordPaymentResult(order.id, result);

  res.json({
    status: result.status,
    paymentId: result.paymentId
  });
});

实际参数、状态名称和幂等机制由第三方 API 决定,不能直接照搬这段示例。

4. 保存令牌,而不是保存信用卡

如果用户选择“保存此卡”,应由支付服务商将卡片保存在其 vault 中,再返回一个与客户账户关联的引用,例如:

{
  "customerId": "cus_opaque_id",
  "paymentMethodId": "pm_opaque_id",
  "brand": "visa",
  "last4": "4242",
  "expiryMonth": 12,
  "expiryYear": 2028
}

自有数据库可以保存:

  • 支付服务商返回的 customerId
  • 不可逆的 paymentMethodId
  • 卡品牌
  • 卡号后四位
  • 有效期,用于界面展示和提醒
  • 用户是否同意保存支付方式
  • 创建时间、状态和撤销时间

不要保存:

  • 完整 PAN(完整卡号)
  • CVV/CVC
  • 磁条数据
  • PIN 或 PIN block
  • 支付服务商的长期私钥
  • 包含上述数据的请求正文、日志或崩溃报告

数据库记录可以采用类似的结构:

{
  "userId": "user_123",
  "providerCustomerId": "cus_opaque_id",
  "providerPaymentMethodId": "pm_opaque_id",
  "brand": "visa",
  "last4": "4242",
  "expiryMonth": 12,
  "expiryYear": 2028,
  "status": "active"
}

令牌通常无法还原成卡号,但仍是需要妥善保护的支付凭据。系统应通过访问控制将令牌与用户账户绑定,不能允许客户端提交任意令牌后直接扣款。

多件商品和多次下单的处理方式

一次结账购买多件商品时,应先在服务端创建订单并计算总金额,然后只执行一次付款,不需要为每件商品重复收集卡片。

同一会话中有多笔交易时,可以让令牌在短时间内保持有效,但每次扣款仍要创建独立订单,并使用幂等键防止网络重试造成重复扣款。

如果用户以后再次下单,则使用保存在支付服务商 vault 中的支付方式:

用户选择“Visa •••• 4242”
        ↓
App 提交 paymentMethodId 和订单号
        ↓
后端验证订单、用户及金额
        ↓
支付服务商使用 vault 中的卡片完成付款

某些地区、发卡行或交易类型可能要求 3-D Secure、强客户认证或重新输入 CVV。遇到这种情况,必须根据支付服务商返回的状态启动认证流程,不能保证所有后续支付都能在无交互的情况下完成。

如果第三方 API 必须接收原始卡号

先向第三方确认以下事项:

  • 是否提供官方移动端 SDK 或托管支付页面
  • 是否支持 tokenization 或 card vault
  • 是否能返回可用于后续付款的 customer/payment method token
  • 是否允许令牌在多个订单中复用
  • 商户与平台分别承担哪些 PCI DSS 责任
  • 是否有 Attestation of Compliance 等可核验的合规材料
  • 是否支持 3-D Secure、退款、撤销和幂等请求
  • 是否明确允许从不可信客户端直接调用接口

如果 App 自行收集完整卡号并直接发送给 API,即使没有持久化,应用及相关流程通常仍会进入 PCI DSS 评估范围。具体范围取决于支付架构、服务商方案和收单机构要求,需要请合格的支付安全或 PCI 顾问确认。

如果确实必须使用原始卡号,至少还要做到:

  • 只使用现代 TLS,并正确校验证书和主机名
  • 禁止明文 HTTP 和降级请求
  • 不把长期商户密钥嵌入 App
  • 不记录请求头、请求正文或表单字段
  • 禁止卡号进入分析、客服录屏和崩溃上报
  • 不使用剪贴板自动填充卡号
  • 屏蔽应用切到后台时的任务快照
  • 卡号只在完成请求所需的最短时间内存在于内存
  • 付款完成后立即释放相关对象

这些措施仍然无法支持安全的长期复用,也不能替代 PCI DSS 合规。更稳妥的设计仍是使用托管支付组件和令牌化。

不要自行设计“加密保存卡号”

即使经过加密,将卡号存放在本地或服务器上仍属于保存持卡人数据。系统还必须处理密钥存储与轮换、权限隔离、审计、备份、日志、灾难恢复和内部人员访问等问题。

Keychain、Android Keystore 或硬件安全模块可以保护密钥,却不会自动让保存完整卡号变得合规。它们更适合保存登录令牌、设备密钥,或在确有需要时保护不可逆的支付令牌,不适合保存原始信用卡信息。

最终应按以下方式划分数据边界:

App 和自有后端:
订单、金额、用户身份、支付令牌、卡品牌、后四位

支付服务商的 PCI DSS 环境:
完整卡号、有效期、CVV 的瞬时处理、卡片 vault

任何一方:
授权完成后都不保存 CVV

如果现有 POS API 无法支持这一边界,应优先更换集成方式,或要求供应商提供令牌化接口,而不是在 App 中自行保存信用卡。

备注:内容仅供参考。