PAYATHON 2026

如何在 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 审核。

推荐的系统结构

一次支付通常按以下顺序完成:

  1. App 将商品或购物车标识发送给业务服务器。
  2. 服务器根据数据库中的价格重新计算金额。
  3. 服务器通过支付服务商的私钥 API 创建支付会话。
  4. 服务器把支付会话的客户端凭证返回给 App。
  5. App 使用支付服务商的官方 SDK 展示信用卡或 Apple Pay 界面。
  6. SDK 将卡信息直接提交给支付服务商,完成令牌化和必要的身份验证。
  7. 支付服务商通过 webhook 将最终结果通知服务器。
  8. 服务器验证 webhook,将订单更新为已支付、失败或待处理。
  9. 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 处理跳转和回调,不要自行模拟银行验证页面。

支付结果也不能只分为成功和失败。常见状态还包括:

  • pending
  • requires_action
  • processing
  • succeeded
  • failed
  • canceled
  • refunded

具体状态名称由支付服务商决定。

防止重复扣款

创建支付时应使用稳定的幂等键,例如订单 ID。请求开始后,可以暂时禁用支付按钮,但这只能防止界面上的重复操作。真正的防重复机制必须在服务器和支付服务商一侧实现。

保护账户和订单接口

至少应采取以下措施:

  • 用户身份认证
  • 订单所有权校验
  • 请求频率限制
  • 服务端输入校验
  • 操作审计
  • 密钥定期轮换
  • 最小权限控制
  • 异常支付监控

日志中的 token、Authorization header 和个人信息应做脱敏处理。

Apple Pay 的作用

Apple Pay 可以减少用户手动输入卡号的操作,并对支付凭证进行令牌化,但它不是独立的收款后台。通常仍需要支付处理商或收单机构处理授权、结算和退款。

接入时一般需要:

  1. 在 Apple Developer 中配置 Merchant ID。
  2. 为 App 启用 Apple Pay capability。
  3. 配置支付处理证书或服务商要求的相关凭据。
  4. 通过 PassKit 或支付服务商 SDK 展示支付表单。
  5. 将生成的支付 token 交给服务端或支付 SDK 处理。

具体配置取决于支付服务商和商户所在地区,应同时查阅 Apple 与支付服务商的当前文档。

上线前的最低检查清单

正式收款前,至少完成以下验证:

  • 使用服务商 sandbox 测试成功、拒付、超时和身份验证场景。
  • 验证重复点击不会产生多笔扣款。
  • 验证 App 被强制关闭后,订单仍能通过 webhook 正确更新。
  • 验证重复或乱序的 webhook 不会导致重复发货。
  • 验证客户端修改金额不会影响服务器实际扣款金额。
  • 验证日志、崩溃报告和分析平台中没有卡号、CVV 或密钥。
  • 验证退款、取消、部分退款和争议处理流程。
  • 完成适用于自身接入方式的 PCI DSS 合规评估。
  • 核对隐私政策、数据保留规则和当地支付法规。
  • 核对当前 App Store 支付规则,以及所选服务商对生产环境的要求。

可以先选择一家覆盖目标国家、币种和结算方式的支付服务商,再按照其官方 iOS SDK 和服务端 SDK 完成集成。不要从信用卡输入框和扣款协议开始自行实现底层支付系统。将敏感数据交给合规组件处理,才能把风险控制在合理范围内。

备注:内容仅供参考。