iOS Client 能否绕过 In-App Purchase 实现远程支付?
结论
可以,前提是支付对象属于在应用之外消费的实体商品、线下服务、账单或金融交易。
在这种场景下,iOS Client 只负责用户认证、展示待付款项、提交支付指令和查询结果。实际扣款由服务端与银行或支付机构完成,无需经过 Apple 的 In-App Purchase 服务器。这种架构通常不会仅因“绕过 IAP”而被拒绝。
审核判断的重点不是交易发生在客户端还是服务端,而是用户购买的具体内容:
- 实体商品、交通、餐饮、线下服务、公共事业账单等,通常可以使用银行卡、Apple Pay 或服务端支付。
- 在应用内解锁或消费的数字内容、会员权益、虚拟货币、游戏功能等,通常必须使用 In-App Purchase。将扣款转移到服务端,并不会改变商品的数字属性。
- 受监管的金融服务除了要遵守支付规则,还可能涉及经营资质、地区合规、身份验证和数据保护要求。
App Store 审核规则会调整,部分要求也会因国家或地区而异,因此无法保证某个具体应用一定能通过审核。提交前,应根据目标 storefront 核对当时有效的 App Review Guidelines。
推荐的系统架构
客户端不应直接保存银行账户密码、银行卡敏感信息或银行接口密钥。可以按以下方式划分职责:
iOS Client
│
├── 登录并取得短期访问令牌
├── 获取待付款项
├── 向业务服务器提交支付确认
└── 查询或接收支付结果
│
▼
Business Server
│
├── 验证用户和账单归属
├── 创建支付请求并保证幂等
├── 调用银行或支付服务商 API
├── 接收支付机构的 webhook
└── 保存最终支付状态
│
▼
Bank / Payment Service Provider
支付处理最好采用异步流程。银行接口返回“请求已接收”,不一定意味着资金已经结算,因此客户端不能仅凭一次 HTTP 响应就显示“支付成功”。
常见的状态模型如下:
pending -> processing -> succeeded
└-> failed
└-> requires_action
服务端应是支付状态的唯一可信来源。推送通知只能提醒客户端刷新,不能作为资金到账的最终凭证。
服务端接口设计
可以提供以下接口:
POST /v1/sessions
GET /v1/payments?status=pending
POST /v1/payments/{paymentId}/confirm
GET /v1/payments/{paymentId}
提交支付时应使用幂等键,防止用户连续点击或网络重试造成重复扣款:
POST /v1/payments/pay_123/confirm
Authorization: Bearer <access-token>
Idempotency-Key: 17E85127-9089-4836-9321-0F36949C7C86
Content-Type: application/json
{
"expectedAmount": {
"currency": "CNY",
"minorUnits": 12800
}
}
服务端必须根据账单重新计算金额。客户端传入的金额只能用于一致性检查,不能直接作为实际扣款金额。
服务端可以先返回:
{
"paymentId": "pay_123",
"status": "processing"
}
银行处理完成后,通过 webhook 通知服务端。服务端验证 webhook 签名并更新数据库,客户端再通过查询接口获取最终结果。
iOS 客户端示例
下面的示例只提交支付确认,不在设备上处理银行交易:
import Foundation
struct Money: Codable {
let currency: String
let minorUnits: Int
}
struct ConfirmPaymentRequest: Codable {
let expectedAmount: Money
}
struct PaymentResponse: Codable {
let paymentId: String
let status: String
}
enum PaymentError: Error {
case invalidResponse
case server(statusCode: Int)
}
func confirmPayment(
paymentId: String,
accessToken: String,
amount: Money
) async throws -> PaymentResponse {
let encodedPaymentId = paymentId.addingPercentEncoding(
withAllowedCharacters: .urlPathAllowed
) ?? paymentId
guard let url = URL(
string: "https://api.example.com/v1/payments/\(encodedPaymentId)/confirm"
) else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue(
"Bearer \(accessToken)",
forHTTPHeaderField: "Authorization"
)
request.setValue(
UUID().uuidString,
forHTTPHeaderField: "Idempotency-Key"
)
request.setValue(
"application/json",
forHTTPHeaderField: "Content-Type"
)
request.httpBody = try JSONEncoder().encode(
ConfirmPaymentRequest(expectedAmount: amount)
)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw PaymentError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw PaymentError.server(statusCode: httpResponse.statusCode)
}
return try JSONDecoder().decode(
PaymentResponse.self,
from: data
)
}
在生产环境中,不要为每次网络重试重新生成幂等键。同一次用户支付操作应复用同一个键,否则服务端可能把重试当成新的支付请求。
最终状态可以通过轮询查询:
func waitForPaymentResult(
paymentId: String,
accessToken: String
) async throws -> PaymentResponse {
for _ in 0..<10 {
let payment = try await fetchPayment(
paymentId: paymentId,
accessToken: accessToken
)
switch payment.status {
case "succeeded", "failed":
return payment
case "pending", "processing", "requires_action":
try await Task.sleep(for: .seconds(2))
default:
throw PaymentError.invalidResponse
}
}
throw URLError(.timedOut)
}
轮询只是其中一种方式。实际项目可以结合 APNs:服务端收到银行 webhook 后发送静默推送或普通通知,客户端收到通知后重新查询支付接口。不要直接把推送消息中的 status 当作支付凭证。
可能触发审核问题的情况
支付对象实际是数字内容
如果付款后会解锁应用内功能、数字订阅、游戏道具、虚拟货币或内容访问权,服务端代扣通常仍会被视为规避 In-App Purchase。商品性质并不取决于交易是否经过 Apple 服务器。
界面引导用户到外部购买数字商品
即使应用本身不收款,在应用内放置外部支付按钮、价格说明或购买引导,也可能受到相关规则限制。具体允许哪些方式,取决于应用类型、目标地区和当时有效的政策。
应用只是一个支付入口
如果应用功能过于单薄,只展示网页、跳转到支付页或提供有限的消息交换,即使支付方式合规,也可能因功能完整性、用户价值或网页套壳等问题受到审核关注。应用应提供清晰的账单详情、交易记录、状态追踪、错误恢复和客服入口。
描述不清导致审核人员误判
App Store Connect 的审核备注应明确说明:
- 支付对象是什么;
- 商品或服务在哪里消费;
- 付款不会解锁应用内数字功能;
- 客户端只向自有服务器提交指令;
- 服务端如何与银行或支付机构通信;
- 审核人员如何使用测试账户验证完整流程。
如果测试环境不会真实扣款,还应说明测试状态的生成方式,并提供所有必要的测试凭据。
安全与合规注意事项
- 所有通信都应使用 HTTPS,并正确验证服务器证书。
- 不要在应用、日志、推送负载或分析事件中记录银行密码、完整卡号、CVV 或访问令牌。
- 支付接口必须确认用户拥有对应账单,不能只检查
paymentId是否存在。 - 服务端保存金额时应使用最小货币单位,例如人民币分,避免使用浮点数。
- 确认支付接口需要实施幂等控制、重放防护、速率限制和审计记录。
- 银行 webhook 必须验证签名、时间戳和事件标识,同时处理重复通知和乱序通知。
- 界面上应明确区分“请求已提交”“银行处理中”和“支付成功”。
- 涉及银行卡数据时,应评估 PCI DSS 等要求。使用支付服务商的 tokenization,通常可以缩小敏感数据的处理范围。
- 如果服务属于汇款、钱包、借贷、证券或其他受监管业务,还需确认运营主体、许可范围和上架地区是否匹配。
只要付款针对应用之外的商品、服务或账单,且客户端不会通过外部支付解锁应用内数字权益,由服务端完成交易并由 iOS Client 展示结果的方案通常是合理的。需要特别避免的是把原本应使用 In-App Purchase 的数字交易改名为“远程支付”,因为审核关注的是交易实质,而不是技术调用链。
备注:内容仅供参考。
