如何在 iOS 应用中集成 eProcessing Network SDK?
明确结论
如果 eProcessing Network 没有提供可直接使用的 iOS SDK,不要让应用直接调用支付网关接口。建议采用以下集成方式:
- iOS 应用向业务服务器发起支付请求。
- 服务器调用 eProcessing Network 的支付接口,或创建托管支付页面。
- iOS 打开托管页面,或者提交由合规组件生成的支付令牌。
- 服务器验证支付结果,并通过回调或订单查询确认最终状态。
网关账号、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"
}
服务器需要完成以下工作:
- 验证用户和订单。
- 从数据库读取应付金额。
- 使用保存在服务器上的网关凭据创建支付会话。
- 返回托管支付页面 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 应位于服务端支付链路中,并优先使用其托管支付或令牌化功能。只有取得与商户账户对应的接口文档,才能确定真实的请求地址、认证方法和字段定义。
备注:内容仅供参考。