PAYATHON 2026

如何在用户资料中安全显示 Credit Card 信息

支付小周

明确结论

不要让浏览器使用支付网关的私密 API 凭证查询卡片,也不要在资料页面返回完整卡号或 CVV。

建议采用以下流程:

  1. Hosted payment fields 将卡片信息直接提交给支付网关。
  2. 支付网关返回 payment_method_id、卡片 token 或 customer reference。
  3. 服务端保存这个无法还原卡号的引用。
  4. 用户打开资料页面时,服务端调用支付网关 API。
  5. 服务端只向前端返回展示所需的脱敏字段,例如 card brand、last four digits 和 expiration date。

典型响应如下:

{
  "brand": "visa",
  "last4": "4242",
  "expMonth": 12,
  "expYear": 2028
}

即使页面只需要显示 card type 和 expiration date,最好也显示 last4。否则,用户绑定多张同品牌卡片后很难区分。

为什么不应该从客户端直接查询

“信用卡数据从 client 发送到 server”并不必然违反 PCI DSS。是否合规取决于传输了什么数据、数据经过哪些系统,以及这些系统是否属于相应的 PCI DSS 范围。

Hosted payment fields 可以让完整卡号和 CVV 从支付网关托管的输入框直接进入支付网关,尽量不经过你的服务器。这通常能缩小 PCI DSS 评估范围。不过,具体适用哪种 SAQ、当前集成方式是否满足要求,仍应以支付网关文档、收单机构要求和合规顾问意见为准。

让浏览器直接查询支付网关会带来几个问题:

  • 私密 API key 无法安全地保存在 JavaScript 中。
  • 用户可能修改请求,查询不属于自己的 customer 或 payment method。
  • 网关响应可能包含页面不需要的字段。
  • 浏览器与支付网关直接集成后,权限控制、日志泄露和跨域配置的风险都会增加。

有些网关提供只能在客户端使用的 publishable key,以及受限的 client token 或 ephemeral key。如果网关明确要求采用这种模式,应按照其官方流程实现。但 secret key、merchant credential 或长期有效的访问令牌不能交给客户端。

推荐的数据流

Hosted payment fields
        │
        │ 卡号、有效期、CVV
        ▼
Payment Gateway
        │
        │ payment_method_id / token
        ▼
Your Server
        │
        │ 保存用户与支付方式引用的绑定关系
        ▼
Profile Page
        │
        │ 请求脱敏后的卡片摘要
        ▼
Your Server ──────► Payment Gateway API
        │
        └─────────► brand、last4、exp_month、exp_year

本地数据库只需保存引用关系:

user_id
gateway_customer_id
gateway_payment_method_id
is_default
created_at

以下内容不要保存:

  • 完整 PAN
  • CVV、CVC 或 CID
  • magnetic-stripe track data
  • PIN 或 PIN block
  • 支付网关的 secret key 到前端可访问的位置

特别是 CVV。PCI DSS 不允许在授权完成后保存 CVV,即使经过加密也不行。

服务端获取卡片摘要

不同支付网关使用的 API 名称和返回结构并不相同。下面是一个通用的 Node.js/Express 示例。gateway.paymentMethods.retrieve() 只是占位接口,需要替换为所用网关官方 SDK 中的对应方法。

app.get("/api/profile/payment-method", requireLogin, async (req, res) => {
  const binding = await db.paymentMethods.findDefaultByUserId(req.user.id);

  if (!binding) {
    return res.status(404).json({
      error: "No payment method on file"
    });
  }

  const paymentMethod = await gateway.paymentMethods.retrieve(
    binding.gatewayPaymentMethodId
  );

  // 必须验证该支付方式确实属于当前用户对应的网关 customer。
  if (paymentMethod.customerId !== binding.gatewayCustomerId) {
    return res.status(403).json({
      error: "Payment method ownership mismatch"
    });
  }

  res.set("Cache-Control", "no-store");

  return res.json({
    brand: paymentMethod.card.brand,
    last4: paymentMethod.card.last4,
    expMonth: paymentMethod.card.expMonth,
    expYear: paymentMethod.card.expYear
  });
});

网关返回的数据结构可能如下:

{
  "id": "pm_example",
  "customerId": "cus_example",
  "card": {
    "brand": "visa",
    "last4": "4242",
    "expMonth": 12,
    "expYear": 2028
  }
}

服务端应设置明确的字段白名单,不要将网关的原始响应直接透传给浏览器。否则,SDK 升级或网关增加字段后,可能意外暴露更多账户数据。

前端显示方式

资料页面只调用自己的后端接口:

async function loadPaymentMethod() {
  const response = await fetch("/api/profile/payment-method", {
    method: "GET",
    credentials: "same-origin",
    headers: {
      "Accept": "application/json"
    }
  });

  if (response.status === 404) {
    return null;
  }

  if (!response.ok) {
    throw new Error("Unable to load payment method");
  }

  return response.json();
}

渲染时使用普通文本节点,不要拼接未经处理的 HTML:

const card = await loadPaymentMethod();

if (card) {
  document.querySelector("#card-summary").textContent =
    `${card.brand.toUpperCase()} •••• ${card.last4} — ` +
    `expires ${String(card.expMonth).padStart(2, "0")}/${card.expYear}`;
}

页面可以显示为:

VISA •••• 4242 — expires 12/2028

是否可以在本地保存这些摘要字段

通常可以在本地保存支付网关返回的 card brand、last4 和 expiration date,以减少 API 调用并加快页面加载。不过,需要注意以下两点。

第一,PCI DSS 的核心适用范围取决于 PAN 的存储、处理或传输等情况。截断后的卡号摘要通常不等同于完整 PAN,但 expiration date 属于支付账户相关数据,仍需采取合理的访问控制和保护措施。具体合规范围不能只根据字段名称判断,还要考虑完整的数据流、系统连接关系、支付网关集成方式和收单机构要求。

第二,本地缓存的信息可能过时。例如,用户在支付网关更新或更换卡片后,本地数据不会自动同步。可以采用以下任一方式:

  • 每次打开资料页面时实时查询网关。
  • 在绑定或更新卡片后保存摘要,再通过 webhook 同步变更。
  • 使用短期缓存,过期后重新查询。

无论采用哪种方式,支付时都应使用网关中的 payment method reference,不能根据本地保存的有效期判断卡片是否可用。

安全与合规注意事项

  • 资料接口必须要求用户登录,并验证 payment method 与当前用户之间的绑定关系。
  • 不要接收前端任意传入的 customer_id 并直接查询。应根据当前登录用户,从服务端数据库中取得对应 ID。
  • 网关 secret key 只能保存在服务端的密钥管理系统或受保护的环境变量中。
  • 全程使用 HTTPS,并配置安全的 session cookie。
  • 不要在应用日志、错误追踪、分析事件或客服系统中记录网关的完整响应。
  • 为资料接口设置合理的速率限制,并考虑使用 Cache-Control: no-store。
  • 更新卡片时,仍应使用网关提供的 hosted fields、hosted page 或安全 SDK。
  • 不要在自己的表单中收集 CVV,用于”验证已绑定卡片”。
  • 接收 webhook 时必须验证签名,并妥善处理重复事件和乱序事件。
  • 确认所用支付网关允许展示这些字段,同时遵循其品牌名称、卡片标识和数据保留规则。

原则很明确:完整支付凭证留在支付网关。你的系统只保存不可逆的支付方式引用,再根据用户权限获取、筛选并展示必要的卡片摘要信息。

备注:内容仅供参考。