如何在用户资料中安全显示 Credit Card 信息
明确结论
不要让浏览器使用支付网关的私密 API 凭证查询卡片,也不要在资料页面返回完整卡号或 CVV。
建议采用以下流程:
- Hosted payment fields 将卡片信息直接提交给支付网关。
- 支付网关返回
payment_method_id、卡片 token 或 customer reference。 - 服务端保存这个无法还原卡号的引用。
- 用户打开资料页面时,服务端调用支付网关 API。
- 服务端只向前端返回展示所需的脱敏字段,例如 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 时必须验证签名,并妥善处理重复事件和乱序事件。
- 确认所用支付网关允许展示这些字段,同时遵循其品牌名称、卡片标识和数据保留规则。
原则很明确:完整支付凭证留在支付网关。你的系统只保存不可逆的支付方式引用,再根据用户权限获取、筛选并展示必要的卡片摘要信息。
备注:内容仅供参考。
