如何在 iPhone application 中实现 payment-gateway?
结论
不要直接在 iPhone 客户端中编写信用卡采集、保存和扣款逻辑。应根据销售内容选择支付方案:
- 销售在 App 内使用的数字内容、功能或订阅时,通常应使用 Apple 的
StoreKit和 App Store In-App Purchase。 - 销售实体商品或线下服务时,可以接入支持 iOS 的合规支付服务商,使用其官方 SDK、托管支付页或 Apple Pay。
- iPhone 只负责展示支付界面和接收一次性支付凭证。创建订单、确认金额、调用支付接口、处理回调等逻辑必须放在自己的服务器上。
- 除非团队已具备相应的 PCI DSS 合规能力,否则不要让原始卡号、有效期和 CVV 经过自己的服务器。
“自己写一套 payment gateway”远不止完成几个 API 请求,还要处理 PCI DSS、令牌化、3-D Secure、欺诈检测、退款、争议和支付对账。对绝大多数应用而言,接入成熟的支付服务商更安全,也更容易维护。
先确认是否允许使用第三方支付
选择技术方案前,先确定应用销售的是什么。
数字内容或 App 内功能
例如:
- 会员订阅
- 虚拟币
- 解锁高级功能
- 数字课程或媒体内容
- App 内消耗的其他数字商品
这类交易通常需要使用 StoreKit。地区、业务类型或外部购买链接是否适用例外,应以应用提交时生效的最新 App Store Review Guidelines 为准。
实体商品或现实世界服务
例如:
- 电商实物
- 外卖
- 出租车
- 酒店
- 线下预约服务
这类业务通常可以使用第三方 payment gateway,例如支持 Apple Pay、信用卡令牌化和 iOS SDK 的支付服务商。
支付方式选错后,即使能够完成扣款,也可能无法通过 App Store 审核。
推荐的系统结构
一次支付通常按以下顺序完成:
- App 将商品或购物车标识发送给业务服务器。
- 服务器根据数据库中的价格重新计算金额。
- 服务器通过支付服务商的私钥 API 创建支付会话。
- 服务器把支付会话的客户端凭证返回给 App。
- App 使用支付服务商的官方 SDK 展示信用卡或 Apple Pay 界面。
- SDK 将卡信息直接提交给支付服务商,完成令牌化和必要的身份验证。
- 支付服务商通过 webhook 将最终结果通知服务器。
- 服务器验证 webhook,将订单更新为已支付、失败或待处理。
- App 从服务器查询订单状态并显示结果。
客户端提交的是“买什么”,不能决定可信的最终金额。金额、币种、折扣和订单状态必须由服务器确定。
iPhone 客户端示例
下面的 Swift 代码演示 App 如何向自己的服务器申请支付会话。接口名称仅作示例,需要根据实际后端调整。
import Foundation
struct CreatePaymentRequest: Encodable {
let orderId: String
}
struct CreatePaymentResponse: Decodable {
let paymentSessionId: String
let clientToken: String
}
enum PaymentAPIError: Error {
case invalidResponse
case requestFailed(Int)
}
func createPaymentSession(
orderId: String
) async throws -> CreatePaymentResponse {
let url = URL(string: "https://api.example.com/payments/sessions")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// 实际项目还应携带登录态,例如短期 access token。
request.httpBody = try JSONEncoder().encode(
CreatePaymentRequest(orderId: orderId)
)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw PaymentAPIError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw PaymentAPIError.requestFailed(httpResponse.statusCode)
}
return try JSONDecoder().decode(
CreatePaymentResponse.self,
from: data
)
}
取得 clientToken 后,将它交给所选支付服务商的官方 iOS SDK。各服务商的初始化方式不同,下面的代码只说明调用边界,不是可以直接编译的通用支付 API:
let session = try await createPaymentSession(orderId: order.id)
// 使用支付服务商提供的官方 SDK 展示其支付组件。
// 原始卡号和 CVV 应直接由 SDK 发送给支付服务商。
paymentProvider.presentCheckout(
clientToken: session.clientToken,
from: viewController
)
不要使用自己的 UITextField 收集卡号,再通过普通 JSON 接口发给服务器。这会大幅扩大 PCI DSS 合规范围,也增加敏感数据通过日志、崩溃报告或调试工具泄露的风险。
服务端示例
下面用 Node.js 风格的伪代码说明客户端和服务端的职责划分。gateway 代表所选服务商的服务端 SDK,具体方法名和参数应以该服务商的官方文档为准。
app.post("/payments/sessions", authenticateUser, async (req, res) => {
const { orderId } = req.body;
const order = await database.orders.findById(orderId);
if (!order || order.userId !== req.user.id) {
return res.status(404).json({ error: "Order not found" });
}
if (order.status !== "pending") {
return res.status(409).json({ error: "Order is not payable" });
}
// 从数据库重新计算,不能采用客户端传入的金额。
const amount = await calculateOrderAmount(order);
const paymentSession = await gateway.createPayment({
amount,
currency: order.currency,
reference: order.id,
idempotencyKey: `order:${order.id}`
});
await database.payments.create({
orderId: order.id,
providerPaymentId: paymentSession.id,
status: "pending"
});
res.json({
paymentSessionId: paymentSession.id,
clientToken: paymentSession.clientToken
});
});
支付服务商的私钥只能保存在服务器中,例如通过 secret manager 或受保护的环境变量加载。任何能从 App 中提取的值都不能视为秘密,所以不能把服务端私钥写进 Swift 代码、配置文件或应用资源。
使用 webhook 确认最终支付结果
客户端显示“支付成功”,不代表订单已经可靠入账。App 可能被关闭,网络也可能在扣款后中断,因此服务器应主要依据经过验证的 webhook 确认结果。
app.post(
"/webhooks/payment-provider",
rawBodyMiddleware,
async (req, res) => {
let event;
try {
event = gateway.verifyWebhook({
rawBody: req.body,
signature: req.headers["payment-signature"]
});
} catch {
return res.status(400).send("Invalid signature");
}
// event.id 应建立唯一索引,避免同一通知被重复处理。
if (await database.webhookEvents.exists(event.id)) {
return res.sendStatus(200);
}
await database.transaction(async (tx) => {
await tx.webhookEvents.create({ id: event.id });
if (event.type === "payment.succeeded") {
await tx.orders.markPaid(event.data.reference);
}
if (event.type === "payment.failed") {
await tx.orders.markPaymentFailed(event.data.reference);
}
});
res.sendStatus(200);
}
);
事件名称、签名请求头和验证方法同样只是示意。实际实现必须采用支付服务商规定的签名验证算法,并使用未经 JSON 解析修改的原始请求体。
webhook 还可能发生以下情况:
- 重复投递
- 延迟到达
- 顺序颠倒
- 网络超时后重试
处理逻辑必须具备幂等性,不能因为收到两次成功通知而发货两次。
必须落实的安全措施
不保存敏感卡信息
不要在 App、服务器、数据库、分析系统或日志中保存:
- 完整卡号
- CVV/CVC
- 磁道数据
- 支付服务商的私钥
所谓“保存银行卡”,通常是保存支付服务商返回的 payment method token,以及用于展示的卡品牌、后四位和有效期。CVV 不应保存。
全程使用 HTTPS
App 与业务服务器之间、服务器与支付服务商之间都应使用 TLS。生产环境不能关闭证书验证,也不能为绕过测试问题而接受任意证书。
ATS、证书固定等措施可以加强部分场景下的防护,但无法替代正确的服务端鉴权、密钥管理和支付签名验证。
不信任客户端数据
客户端可能被修改,因此服务器不能直接相信客户端提交的以下数据:
- 金额
- 币种
- 折扣
- 商品价格
- 支付成功标志
- 用户或订单归属
- 支付服务商返回对象的文本副本
服务器应根据商品 ID 和订单内容自行计算应付金额,并通过支付服务商的可信接口或经过验证的 webhook 获取支付状态。
支持 3-D Secure 等验证流程
部分地区、银行或交易要求使用 3-D Secure、SCA 或其他额外验证。应通过支付服务商的官方 SDK 处理跳转和回调,不要自行模拟银行验证页面。
支付结果也不能只分为成功和失败。常见状态还包括:
pendingrequires_actionprocessingsucceededfailedcanceledrefunded
具体状态名称由支付服务商决定。
防止重复扣款
创建支付时应使用稳定的幂等键,例如订单 ID。请求开始后,可以暂时禁用支付按钮,但这只能防止界面上的重复操作。真正的防重复机制必须在服务器和支付服务商一侧实现。
保护账户和订单接口
至少应采取以下措施:
- 用户身份认证
- 订单所有权校验
- 请求频率限制
- 服务端输入校验
- 操作审计
- 密钥定期轮换
- 最小权限控制
- 异常支付监控
日志中的 token、Authorization header 和个人信息应做脱敏处理。
Apple Pay 的作用
Apple Pay 可以减少用户手动输入卡号的操作,并对支付凭证进行令牌化,但它不是独立的收款后台。通常仍需要支付处理商或收单机构处理授权、结算和退款。
接入时一般需要:
- 在 Apple Developer 中配置 Merchant ID。
- 为 App 启用 Apple Pay capability。
- 配置支付处理证书或服务商要求的相关凭据。
- 通过
PassKit或支付服务商 SDK 展示支付表单。 - 将生成的支付 token 交给服务端或支付 SDK 处理。
具体配置取决于支付服务商和商户所在地区,应同时查阅 Apple 与支付服务商的当前文档。
上线前的最低检查清单
正式收款前,至少完成以下验证:
- 使用服务商 sandbox 测试成功、拒付、超时和身份验证场景。
- 验证重复点击不会产生多笔扣款。
- 验证 App 被强制关闭后,订单仍能通过 webhook 正确更新。
- 验证重复或乱序的 webhook 不会导致重复发货。
- 验证客户端修改金额不会影响服务器实际扣款金额。
- 验证日志、崩溃报告和分析平台中没有卡号、CVV 或密钥。
- 验证退款、取消、部分退款和争议处理流程。
- 完成适用于自身接入方式的 PCI DSS 合规评估。
- 核对隐私政策、数据保留规则和当地支付法规。
- 核对当前 App Store 支付规则,以及所选服务商对生产环境的要求。
可以先选择一家覆盖目标国家、币种和结算方式的支付服务商,再按照其官方 iOS SDK 和服务端 SDK 完成集成。不要从信用卡输入框和扣款协议开始自行实现底层支付系统。将敏感数据交给合规组件处理,才能把风险控制在合理范围内。
备注:内容仅供参考。
