iPhone 支付应用是否需要符合 PCI 合规要求?
结论
需要考虑 PCI DSS 合规要求。
如果 iPhone 应用直接接收、处理或传输信用卡主账号(PAN)、有效期、持卡人姓名或安全码(CVV/CVC),应用及相关后端、日志、监控和运维环境通常都会被纳入 PCI DSS 评估范围。HTTPS POST 只能保护数据传输,不能让整个支付流程自动满足 PCI DSS。
更稳妥的做法是避免让自己的应用和服务器接触原始卡号。可以使用支付网关提供的官方 iOS SDK、托管支付页面或 Apple Pay,由支付服务商采集卡片信息并返回一次性 token,自己的服务器只用这个 token 发起交易。
这种架构通常可以大幅缩小合规范围。不过,具体适用哪种 SAQ、是否需要额外评估,仍应由收单行、支付网关或合格安全评估机构确认。
为什么 HTTPS 还不够
HTTPS 解决的主要是客户端与服务器之间的数据加密。PCI DSS 检查的是整个支付链路,包括:
- 卡号是否进入应用内存、崩溃报告或调试日志;
- 卡号是否经过自己的服务器、代理、API 网关和消息队列;
- 数据库、缓存和备份是否保存持卡人数据;
- 哪些人可以访问生产环境和支付系统;
- 密钥如何生成、保存、轮换和撤销;
- 应用及服务器是否定期进行漏洞扫描、渗透测试和补丁更新;
- 是否建立了访问审计、异常监控和事件响应流程。
即使请求使用 TLS,只要请求正文中包含原始卡号,由你控制的相关系统通常仍属于持卡人数据环境。
CVV/CVC 属于敏感认证数据,一般不得在授权完成后保存,即使经过加密也不应保留。
推荐的支付架构
优先采用以下流程:
iPhone App
│
│ 通过支付网关官方 SDK 或托管页面录入卡片
▼
Payment Gateway
│
│ 返回一次性 payment token / payment method ID
▼
iPhone App
│
│ 只把 token 和订单号发给自己的服务器
▼
Merchant Backend
│
│ 使用服务端凭据和 token 请求扣款
▼
Payment Gateway
这套架构需要满足几个条件:
- 卡号和 CVV 直接提交给支付网关。
- 自己的后端不接收原始卡片数据。
- iPhone 应用只取得无法直接还原卡号的一次性 token 或支付方式标识。
- 支付网关的私钥只保存在服务器端,不能写入 App。
- 最终支付金额由服务器根据订单重新计算,不能采用客户端提交的金额。
也可以优先考虑 Apple Pay。它能减少用户手工输入卡号的需要,但使用 Apple Pay 并不一定意味着商户完全退出 PCI DSS 范围,仍要看实际支付链路、服务商方案和收单行的要求。
Swift 实现思路
不同支付网关的 SDK 接口并不相同。下面只展示代码结构,不对应任何一家支付服务商的真实 API。实际项目应直接使用所选网关官方 SDK 提供的卡片输入组件和 tokenization 接口,不要自行用普通 UITextField 接收卡号。
struct PaymentTokenRequest: Encodable {
let orderID: String
let paymentToken: String
}
struct PaymentResult: Decodable {
let status: String
let transactionID: String?
}
func submitPayment(
orderID: String,
paymentToken: String
) async throws -> PaymentResult {
let endpoint = URL(string: "https://api.example.com/payments")!
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(
PaymentTokenRequest(
orderID: orderID,
paymentToken: paymentToken
)
)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode) else {
throw PaymentError.requestFailed
}
return try JSONDecoder().decode(PaymentResult.self, from: data)
}
enum PaymentError: Error {
case requestFailed
}
这段代码发送给商户服务器的是 paymentToken,而不是原始卡片信息:
{
"cardNumber": "4111111111111111",
"cvv": "123",
"expiry": "12/30"
}
服务器再通过支付网关的服务端 SDK 或 API,结合订单和 token 发起支付:
POST /payments
{
"orderID": "ORDER-2026-0001",
"paymentToken": "gateway_generated_token"
}
收到请求后,服务器需要通过 orderID 查询真实订单,重新获取金额和币种。不能直接用客户端传来的最终金额扣款,否则攻击者可能篡改请求内容。
如果业务必须直接采集卡号
如果业务确实无法使用支付网关 SDK、托管页面或 tokenization 方案,就应把它当作完整的 PCI DSS 项目处理。仅给 App 增加几个安全设置并不能满足要求。
通常需要完成以下工作。
确定合规责任和评估范围
先与收单行和支付网关确认:
- 商户在支付链路中的角色;
- 哪些移动端、后端和基础设施属于评估范围;
- 适用的 PCI DSS 版本及 SAQ 类型;
- 是否需要由 QSA(Qualified Security Assessor)进行正式评估;
- 是否需要外部漏洞扫描服务商执行扫描。
不能只因为使用了第三方网关就确定 SAQ 类型。如果卡号仍由自己的 App 采集,或者会经过自己的服务器,通常不能按照完全外包卡片采集的场景处理。
尽量减少数据范围
- 不保存 CVV/CVC;
- 没有明确业务需要时,不保存完整 PAN;
- 页面和日志中只显示掩码卡号,例如
************1234; - 清除请求日志、崩溃报告和分析工具中的敏感字段;
- 检查反向代理、WAF、APM、客服系统和备份是否会复制请求正文;
- 在网络和权限层面将支付环境与普通业务系统隔离。
保护必须保存的数据
如果确实需要保存 PAN,应使用符合要求的强加密,并建立完整的密钥管理机制。加密密钥不能与密文存放在同一位置,也不能把解密密钥硬编码到 App 中。
同时还要限制数据保留时间,建立自动删除机制,并记录相关数据的访问人员和访问时间。
建立安全开发流程
至少要覆盖:
- 代码审查和依赖项检查;
- 服务端输入验证与鉴权;
- 防重放和幂等处理;
- 漏洞管理及安全补丁;
- 移动端与服务端渗透测试;
- 生产访问最小权限和多因素认证;
- 审计日志、告警和事件响应;
- 定期确认第三方支付服务商的 PCI DSS 合规状态。
移动端不能视为完全可信的环境。越狱设备、动态注入、调试工具或恶意辅助功能都可能影响输入数据。仅依靠 App 自身的反调试、代码混淆或证书固定,无法满足合规要求。
容易忽略的问题
不要在以下位置输出卡号、CVV 或完整的支付请求:
print(cardNumber)
logger.debug("Payment request: \(requestBody)")
也不要把支付网关的服务端密钥放进 iOS 应用:
let secretKey = "sk_live_xxx" // 错误:App 中的密钥可以被提取
App 中只能保存支付网关明确允许公开使用的客户端标识或 publishable key。能够创建扣款、退款或访问交易数据的密钥必须由服务器保管。
支付结果也不能只依据 App 收到的响应。服务器应验证支付网关返回的结果,并对 webhook 执行签名校验、时间戳检查和幂等处理,以防止伪造通知或重复记账。
实际落地建议
可以按以下顺序实施:
- 选择已经通过相应 PCI DSS 验证的支付服务商。
- 使用其官方 iOS SDK、托管支付页面或 Apple Pay 集成。
- 确保原始卡号和 CVV 不经过自己的服务器。
- 让服务器只接收 token,并在服务端完成扣款。
- 清查日志、崩溃分析、监控和客服工具,确认其中没有持卡人数据。
- 根据真实的数据流图,与收单行或 QSA 确认适用的 SAQ 和验证材料。
- 按收单行要求完成年度验证、扫描或其他持续合规工作。
不要试图只把一个直接处理卡号的 App 做成所谓的 PCI 合规应用。更合适的办法是从架构上避免接触原始卡片数据,再对其余系统完成相应级别的 PCI DSS 验证。
备注:内容仅供参考。
