如何安全获取并存储 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 收集卡片
通常可按以下顺序选择:
- 支付服务商的移动端 SDK
- 托管支付页面
- Web 场景中的 Hosted Fields
- 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 中自行保存信用卡。
备注:内容仅供参考。
