如何在 Android / Blackberry 应用中完成信用卡支付?
结论
Drupal Web Services 可以接入支付流程,但不应直接接收、保存或处理信用卡号、CVV 等敏感数据。建议采用以下流程:
- 移动端向 Drupal 发起注册和订单创建请求。
- Drupal 按服务器端保存的价格创建待支付订单,并向支付服务商创建支付会话。
- 移动端通过支付服务商的官方 SDK 或托管收银台付款。
- 支付服务商通过带签名的 Webhook 通知 Drupal。
- Drupal 验证通知并查询支付结果,确认成功后再激活账户。
这种设计既保留了 Ubercart 的订单管理能力,也缩小了 PCI DSS 的合规范围。移动应用不能自行声明”付款成功”,Drupal 也不能仅凭客户端回调开通账户。
如果注册费对应应用内数字内容、会员功能或服务,Google Play 等应用商店可能要求使用其应用内结算系统。实体商品或在应用外消费的现实服务,通常可以使用第三方信用卡支付。商店政策可能调整,上线前需要核对目标商店、发行地区和业务类型对应的最新规则。
推荐架构
典型流程如下:
Mobile App
|
| 1. 请求注册并创建订单
v
Drupal / Ubercart
|
| 2. 使用服务器端密钥创建支付会话
v
Payment Provider
^
| 3. 官方 SDK / Hosted Checkout
|
Mobile App
Payment Provider
|
| 4. 签名 Webhook
v
Drupal
|
| 5. 验证金额、币种、订单号和支付状态
v
激活账户 / 完成 Ubercart 订单
在这套架构中,Drupal Web Services 负责订单编排和状态查询,不负责银行卡收单。
实现步骤
1. 选择支付渠道
主要有三种方案:
- 应用商店内购:适用于数字会员、数字内容和应用功能解锁。
- 支付服务商原生 SDK:银行卡信息由 SDK 收集,应用和 Drupal 只会接触 token、payment method ID 或支付会话 ID。
- 托管收银台:用户在浏览器、自定义标签页或系统 WebView 之外的页面完成付款,开发和合规成本通常较低。
不要自行开发信用卡输入表单,再通过 Drupal 接口上传完整卡号。即使整个过程都使用 HTTPS,这种做法仍会扩大攻击面,并增加 PCI DSS 合规责任。
2. 由 Drupal 创建注册订单
移动端只提交套餐或商品标识,不提交最终价格:
POST /api/mobile/registration-orders
Authorization: Bearer <access_token>
Content-Type: application/json
{
"plan_id": "standard_registration",
"currency": "USD",
"idempotency_key": "8bf79c80-..."
}
服务器根据 plan_id 从可信配置或商品数据中读取金额,创建 Ubercart 订单,然后返回支付会话:
{
"order_id": "12345",
"payment_session_id": "ps_xxx",
"client_secret": "client_value_if_required",
"amount": 2500,
"currency": "USD",
"status": "pending"
}
这里的 2500 通常是最小货币单位,例如 25.00 USD。实际格式取决于支付服务商。对于采用零位或三位小数的币种,不能按默认规则猜测金额格式。
不同版本的 Ubercart、Drupal 和支付模块,创建订单及更新订单状态的 API 可能不同。所以下面的代码仅用于说明架构,不能直接复制到所有版本中使用:
<?php
function createRegistrationPayment(array $request): array
{
$planId = $request['plan_id'];
$idempotencyKey = $request['idempotency_key'];
// 必须从服务器端读取,不能相信客户端传入的金额。
$plan = loadRegistrationPlan($planId);
if (!$plan) {
throw new InvalidArgumentException('Unknown registration plan');
}
// 同一个幂等键只能生成一个业务订单。
$existingOrder = findOrderByIdempotencyKey($idempotencyKey);
if ($existingOrder) {
return buildPaymentResponse($existingOrder);
}
$order = createUbercartOrder([
'amount' => $plan->amount,
'currency' => $plan->currency,
'status' => 'pending',
'idempotency_key' => $idempotencyKey,
]);
$session = paymentProviderCreateSession([
'amount' => $plan->amount,
'currency' => $plan->currency,
'merchant_order_id' => (string) $order->id,
]);
saveProviderPaymentId($order->id, $session->id);
return [
'order_id' => (string) $order->id,
'payment_session_id' => $session->id,
'client_secret' => $session->clientSecret ?? null,
'amount' => $plan->amount,
'currency' => $plan->currency,
'status' => 'pending',
];
}
支付服务商的私钥只能存放在 Drupal 服务端,不能打包到 Android 或 BlackBerry 应用中。
3. 在移动端调用官方支付组件
Android 端应使用支付服务商支持的 SDK,或打开服务商提供的托管页面。下面的 Kotlin 代码只表示通用调用关系,实际类名和参数应以所选支付服务商当前版本的 SDK 为准:
data class RegistrationOrder(
val orderId: String,
val paymentSessionId: String,
val clientSecret: String?,
val amount: Long,
val currency: String,
val status: String
)
suspend fun startRegistrationPayment(planId: String) {
val order = api.createRegistrationOrder(
planId = planId,
idempotencyKey = UUID.randomUUID().toString()
)
paymentSdk.presentCheckout(
clientSecret = requireNotNull(order.clientSecret)
)
}
即使 SDK 返回成功,客户端也不能直接激活账户,而应向 Drupal 查询订单状态:
suspend fun waitForConfirmation(orderId: String) {
val order = api.getRegistrationOrder(orderId)
when (order.status) {
"paid" -> showRegistrationCompleted()
"pending" -> showPaymentProcessing()
"failed" -> showPaymentFailed()
}
}
客户端可能被篡改、调试或重放,因此移动端回调只能用于更新界面提示,不能作为支付成功的依据。
4. 使用 Webhook 确认付款
付款完成后,支付服务商应向 Drupal 的专用 HTTPS 地址发送通知。Drupal 必须先验证签名,再处理事件:
<?php
function handlePaymentWebhook(
string $rawBody,
string $signatureHeader
): void {
$event = verifyProviderWebhook(
$rawBody,
$signatureHeader,
getWebhookSecret()
);
if ($event->type !== 'payment.succeeded') {
return;
}
$payment = $event->payment;
$order = loadOrderByProviderPaymentId($payment->id);
if (!$order) {
throw new RuntimeException('Order not found');
}
if ($payment->amount !== $order->amount) {
throw new RuntimeException('Amount mismatch');
}
if ($payment->currency !== $order->currency) {
throw new RuntimeException('Currency mismatch');
}
// Webhook 可能重复发送,处理逻辑必须幂等。
if ($order->status === 'paid') {
return;
}
markOrderAsPaidAtomically($order->id, $payment->id);
activateRegisteredAccount($order->userId);
}
“订单标记成功”和”账户激活”最好放在数据库事务中完成,或使用具备并发保护的状态转换,防止重复通知造成重复记账或重复发放权益。
关键安全要求
不信任客户端
客户端提交的以下数据都不能直接作为付款依据:
- 金额和币种
- 商品名称或注册套餐价格
- “支付成功”标志
- 用户 ID 或订单所有者
- 支付服务商返回的数据副本
服务器必须重新计算价格,并确认当前登录用户有权访问对应订单。
不保存银行卡敏感信息
以下信息不应出现在 Drupal、Ubercart 日志、移动端日志或分析平台中:
- 完整银行卡号,即 PAN
- CVV/CVC
- 磁道数据
- 支付服务商私钥
- 可重复使用的身份验证数据
如果业务要求系统直接处理卡号,就需要完成相应的 PCI DSS 评估并部署安全控制。普通移动应用通常不适合自行承担这项工作。
正确处理认证和传输
所有接口都必须使用 HTTPS,并正确验证服务器证书。此外还需要:
- 使用短期访问令牌或可靠的会话认证。
- 分别校验创建订单、查询订单和退款操作的权限。
- 为敏感接口设置速率限制。
- 防止请求重放,并在创建订单时使用幂等键。
- 不在 URL 查询参数中传递 token 或支付信息。
- 不将支付密钥硬编码到 APK、BAR 或其他客户端安装包中。
- 验证 Webhook 的签名、时间戳和事件唯一标识。
- 对异常订单、重复支付和金额不匹配发出告警。
证书固定可以增加一层防护,但也会带来证书轮换和旧客户端失效等问题,而且不能替代正常的 TLS 验证。
正确设计订单状态
建议至少设置以下状态:
pending
requires_action
processing
paid
failed
cancelled
refunded
有些支付方式不会立即完成,或者需要 3-D Secure 等额外验证。因此,用户关闭支付页面不一定表示支付失败,SDK 已提交也不等于付款成功。最终状态应以经过验证的 Webhook 为准。必要时,Drupal 还应通过服务器端 API 主动向支付服务商查询。
Android 与 BlackBerry 的差异
Android 通常可以使用支付服务商的 Android SDK、系统浏览器或 Custom Tabs。如果应用通过 Google Play 分发,还要先确认注册费是否属于必须使用 Google Play Billing 的数字交易。
对于”BlackBerry”,需要先确定具体平台:
- 如果是可以运行 Android 应用的 BlackBerry 设备,可以评估支付 SDK 的 Android 兼容性,但必须实际测试其依赖组件。
- 如果是旧版 BlackBerry OS 或 BB10,许多现代支付服务商已经不再提供原生 SDK。这种情况下,通常只能使用托管收银台,或保留现有的网站支付流程。
- 如果应用通过特定商店发布,还需要遵守该商店关于数字商品和外部支付的规则。
在设备型号、系统版本和应用商店都不明确的情况下,无法保证现代支付 SDK 能直接支持旧 BlackBerry 平台。托管支付页面通常能覆盖更多平台,但仍需确认支付服务商支持的浏览器、TLS 版本和回跳机制。
与 Ubercart 集成时的注意事项
Ubercart 可以继续管理商品、订单和注册权益,但不能默认把网站现有的支付页面接口直接开放给移动端。应为移动端建立边界明确的 API,同时复用服务器端已有的订单规则。
集成时需要检查:
- 移动订单是否进入与网站订单相同的状态机。
- 支付成功后由哪个流程激活 Drupal 用户。
- 支付失败、取消或超时后,系统是否会清理未完成的注册。
- 退款后是否需要撤销会员权限。
- 同一笔支付交易能否绑定到多个账户。
- Webhook 重复或乱序到达时,系统能否得出正确结果。
- 订单和支付记录是否保存了可审计的服务商交易 ID。
如果现有 Ubercart 支付模块只支持网页表单,不具备可靠的服务端 token 化或 Webhook 能力,可以由 Drupal 直接调用支付服务商的服务器端 API,再将结果同步回 Ubercart。也可以使用托管收银台,通过订单号关联 Drupal 订单。
移动端负责展示并调用安全的支付组件,Drupal 负责定价、订单、权限和结果验证,支付服务商负责处理银行卡数据。这套分工比通过通用 Web Service 直接传输信用卡信息更安全,也更便于维护。
备注:内容仅供参考。
