PAYATHON 2026

如何在 iOS 应用中集成 eProcessing Network SDK?

支付阿杰

明确结论

如果 eProcessing Network 没有提供可直接使用的 iOS SDK,不要让应用直接调用支付网关接口。建议采用以下集成方式:

  1. iOS 应用向业务服务器发起支付请求。
  2. 服务器调用 eProcessing Network 的支付接口,或创建托管支付页面。
  3. iOS 打开托管页面,或者提交由合规组件生成的支付令牌。
  4. 服务器验证支付结果,并通过回调或订单查询确认最终状态。

网关账号、API 密钥、交易密钥等敏感凭据必须保存在服务器端,不能写入 iOS 应用。eProcessing Network 的接口地址、认证字段和请求参数可能因商户产品及账户配置而异,应以商户后台提供的最新接口文档为准。

为什么不能从 iOS 直接调用支付网关

移动应用无法安全保存支付网关密钥。即使混淆了相关字符串,攻击者仍可能从安装包、运行时内存或网络请求中提取凭据。

如果应用直接采集和传输完整卡号、有效期及 CVV,通常还会显著扩大 PCI DSS 的合规范围。可以优先考虑以下方案来降低风险:

  • eProcessing Network 提供的 Hosted Payment Page。
  • 官方支持的 tokenization 或 hosted fields。
  • 由支付服务端生成并管理的客户或银行卡令牌。

如果网关只有传统的服务端交易 API,所有信用卡数据都应在经过 PCI DSS 评估的服务器环境中处理。客户端使用 HTTPS,并不代表整个支付流程已经符合合规要求。

推荐的集成流程

1. 向 eProcessing Network 确认产品能力

编码前,先通过商户后台或技术支持确认以下信息:

  • 是否提供移动端专用 SDK。
  • 是否支持 Hosted Payment Page。
  • 是否支持信用卡 tokenization。
  • 沙箱与生产环境的接口地址。
  • 认证方式以及签名规则。
  • 支付成功、失败和处理中状态的定义。
  • 是否支持 webhook,以及如何验证回调签名。
  • 是否支持退款、撤销、预授权和捕获。

如果没有官方文档,不要参照其他网关的字段名称猜测 eProcessing Network 的参数。

2. 在服务器创建支付会话

iOS 只向自己的服务器发送订单编号等业务数据。服务器必须根据订单重新计算金额,不能直接采用客户端提交的值。

示例接口:

POST /api/payments/session
Content-Type: application/json
Authorization: Bearer <user-access-token>

{
  "orderId": "ORDER-20260913-001"
}

服务器需要完成以下工作:

  1. 验证用户和订单。
  2. 从数据库读取应付金额。
  3. 使用保存在服务器上的网关凭据创建支付会话。
  4. 返回托管支付页面 URL,或者返回客户端可用的一次性会话标识。

下面是示意性的 Node.js 代码。网关地址和字段均为占位符,必须按照 eProcessing Network 的实际文档替换:

import express from "express";

const app = express();
app.use(express.json());

app.post("/api/payments/session", async (req, res) => {
  const { orderId } = req.body;

  // 应从数据库读取并校验订单,不要采用客户端传入的金额。
  const order = await loadPayableOrder(orderId);

  const gatewayResponse = await fetch(
    process.env.EPN_PAYMENT_SESSION_ENDPOINT,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.EPN_API_KEY}`
      },
      body: JSON.stringify({
        merchantReference: order.id,
        amount: order.amount,
        currency: order.currency,
        returnUrl: "myapp://payment/return",
        notificationUrl: "https://api.example.com/webhooks/epn"
      })
    }
  );

  if (!gatewayResponse.ok) {
    return res.status(502).json({
      error: "Unable to create payment session"
    });
  }

  const session = await gatewayResponse.json();

  res.json({
    paymentUrl: session.paymentUrl
  });
});

EPN_PAYMENT_SESSION_ENDPOINT、Authorization、merchantReference 和响应中的 paymentUrl 只是用于说明服务端代理模式,并不代表 eProcessing Network 的实际协议。

3. iOS 请求支付会话

iOS 可以使用 URLSession 调用自己的服务器:

import Foundation

struct PaymentSessionResponse: Decodable {
    let paymentUrl: URL
}

enum PaymentError: Error {
    case invalidResponse
}

func createPaymentSession(
    orderId: String,
    accessToken: String
) async throws -> URL {
    let endpoint = URL(
        string: "https://api.example.com/api/payments/session"
    )!

    var request = URLRequest(url: endpoint)
    request.httpMethod = "POST"
    request.setValue(
        "application/json",
        forHTTPHeaderField: "Content-Type"
    )
    request.setValue(
        "Bearer \(accessToken)",
        forHTTPHeaderField: "Authorization"
    )
    request.httpBody = try JSONEncoder().encode([
        "orderId": orderId
    ])

    let (data, response) = try await URLSession.shared.data(for: request)

    guard
        let httpResponse = response as? HTTPURLResponse,
        (200..<300).contains(httpResponse.statusCode)
    else {
        throw PaymentError.invalidResponse
    }

    return try JSONDecoder()
        .decode(PaymentSessionResponse.self, from: data)
        .paymentUrl
}

生产代码还要处理超时、网络中断、服务端错误码和重复请求。

4. 展示托管支付页面

如果 eProcessing Network 返回 HTTPS 托管支付页面,可以使用 ASWebAuthenticationSession。支付完成后需要跳回应用时,应配置自定义 URL Scheme 或 Universal Link。

import AuthenticationServices

final class PaymentWebSession: NSObject,
                               ASWebAuthenticationPresentationContextProviding {
    private var session: ASWebAuthenticationSession?

    func start(
        paymentURL: URL,
        completion: @escaping (Result<URL, Error>) -> Void
    ) {
        session = ASWebAuthenticationSession(
            url: paymentURL,
            callbackURLScheme: "myapp"
        ) { callbackURL, error in
            if let error {
                completion(.failure(error))
                return
            }

            guard let callbackURL else {
                completion(.failure(PaymentError.invalidResponse))
                return
            }

            completion(.success(callbackURL))
        }

        session?.presentationContextProvider = self
        session?.prefersEphemeralWebBrowserSession = true
        session?.start()
    }

    func presentationAnchor(
        for session: ASWebAuthenticationSession
    ) -> ASPresentationAnchor {
        ASPresentationAnchor()
    }
}

部分支付页面可能更适合使用 SFSafariViewController,具体取决于网关对应用回跳、Cookie、重定向和认证的要求。不要默认使用 WKWebView。某些支付机构不允许使用嵌入式 WebView,3-D Secure 等认证流程也可能因此无法正常运行。

不要根据回跳参数直接判定支付成功

下面这样的回跳只能说明用户已经返回应用:

myapp://payment/return?orderId=ORDER-20260913-001

它不能作为到账凭证。客户端参数可能被伪造,而且用户返回应用时,银行认证可能尚未完成。

应用收到回跳后,应向自己的服务器查询订单状态:

struct PaymentStatusResponse: Decodable {
    let status: String
}

func fetchPaymentStatus(
    orderId: String,
    accessToken: String
) async throws -> PaymentStatusResponse {
    let encodedOrderId = orderId.addingPercentEncoding(
        withAllowedCharacters: .urlPathAllowed
    )!

    let url = URL(
        string: "https://api.example.com/api/payments/\(encodedOrderId)"
    )!

    var request = URLRequest(url: url)
    request.setValue(
        "Bearer \(accessToken)",
        forHTTPHeaderField: "Authorization"
    )

    let (data, response) = try await URLSession.shared.data(for: request)

    guard
        let httpResponse = response as? HTTPURLResponse,
        (200..<300).contains(httpResponse.statusCode)
    else {
        throw PaymentError.invalidResponse
    }

    return try JSONDecoder().decode(
        PaymentStatusResponse.self,
        from: data
    )
}

服务器应根据以下信息确定最终状态:

  • 经过签名验证的 webhook。
  • 服务器主动查询网关得到的交易状态。
  • 网关返回的交易 ID、订单号和金额是否与本地订单一致。

如果只能使用直接交易 API

如果商户账户不支持托管页面或 tokenization,只能调用直接信用卡交易接口,应先向 eProcessing Network 和合规顾问确认 PCI DSS 要求。除非相关环境已经按要求建设并通过审核,否则不要让普通应用服务器和日志系统直接处理卡号。

尤其不要这样做:

// 不要将这些数据发送到没有 PCI DSS 保障的普通业务接口。
let card = [
    "cardNumber": "4111111111111111",
    "cvv": "123",
    "expiration": "12/30"
]

卡号、CVV、网关密钥和完整的网关响应也不能写入日志、崩溃报告或分析平台。授权完成后不应保存 CVV。

注意事项

  • 使用沙箱账号进行开发,并将生产凭据与沙箱凭据隔离。
  • 服务端要实现幂等控制,避免网络重试造成重复扣款。
  • 金额应使用最小货币单位或十进制定点类型,不能使用浮点数计算。
  • 订单号必须唯一,同时保存网关交易 ID,以便后续查询和退款。
  • webhook 必须验证签名和时间戳,并处理重复事件,不能只依赖来源 IP。
  • 支付超时不代表支付失败。订单应进入”处理中”状态,再由服务器向网关查询。
  • 是否需要 3-D Secure、AVS、CVV 校验和账单地址,应根据账户配置及业务地区确认。
  • 上线前应测试支付成功、拒付、余额不足、超时、用户取消、重复提交和回调延迟等场景。
  • 使用信用卡购买实体商品或一般服务,通常可以接入外部支付网关。如果销售应用内数字内容或数字功能,还要确认是否需要遵守 Apple 的 In-App Purchase 规则。

缺少 iOS SDK 并不妨碍接入。eProcessing Network 应位于服务端支付链路中,并优先使用其托管支付或令牌化功能。只有取得与商户账户对应的接口文档,才能确定真实的请求地址、认证方法和字段定义。

备注:内容仅供参考。