如何从 Android WebView 获取支付网关的 POST 数据
支付老李
结论
Android WebView 没有可靠的 API 能读取页面导航时提交的原始 POST body。onPageStarted()、onPageFinished() 和 shouldInterceptRequest() 可以观察 URL 或部分请求信息,但无法将支付网关提交的 POST 参数安全、完整地传给 App。
建议按以下流程处理:
- 支付网关将结果 POST 到你的 HTTPS 后端。
- 后端验证签名,并主动向支付网关查询交易状态。
- 后端以幂等方式更新交易并创建订单。
- WebView 到达结果页后,App 调用自己的后端查询最终状态。
WebView 显示“成功页面”只说明 App 可以刷新支付状态,不能证明付款已经成功。
为什么不能直接从 WebView 获取
WebViewClient 的常用回调不会提供 POST body:
onPageStarted()和onPageFinished()只提供页面 URL。WebResourceRequest可以获取 URL、请求方法和部分请求头,但不包含请求体。shouldOverrideUrlLoading()不保证会在 POST 请求发生时调用。loadUrl()和postUrl()用来发起请求,不能读取已经收到的 POST 数据。- 通过 JavaScript 读取结果页,只能访问服务器生成的 DOM,无法取得原始 HTTP POST body。
即使能从页面中提取出 status=success,也不能直接信任,因为这些数据处于客户端环境。用户、恶意网页或遭到篡改的跳转都可能伪造成功页面。
推荐的处理流程
假设每次支付前都已生成唯一的 tempTxnId:
App 创建待支付交易
↓
WebView 打开支付网关
↓
网关 POST /payment/callback
↓
后端验证签名,并向网关查询交易
↓
后端更新交易并幂等创建订单
↓
结果页加载完成
↓
App 请求 GET /api/payments/{tempTxnId}/status
后端可以向 App 返回以下状态:
{
"transactionId": "txn_123",
"status": "PAID",
"orderId": "order_456"
}
其中,PAID 必须来自后端验证过的记录,不能只根据 WebView URL 判断。
后端回调示例
下面是简化后的伪代码。字段名称、签名算法和查询接口应按实际支付网关文档实现:
@PostMapping("/payment/callback")
public ResponseEntity<Void> paymentCallback(
@RequestParam MultiValueMap<String, String> form) {
String transactionId = form.getFirst("transactionId");
String signature = form.getFirst("signature");
if (!paymentGateway.verifySignature(form, signature)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
// 不应只相信回调中的 success 字段。
GatewayPayment payment = paymentGateway.queryPayment(transactionId);
if (payment.isPaid()
&& payment.getAmount().equals(findExpectedAmount(transactionId))
&& payment.getMerchantId().equals(expectedMerchantId)) {
// transactionId 或 gatewayPaymentId 应有唯一约束,
// 防止重复回调创建多个订单。
paymentService.markPaidAndCreateOrderIdempotently(transactionId);
}
return ResponseEntity.status(HttpStatus.SEE_OTHER)
.location(URI.create(
"https://www.mydomain.in/payment/result?transactionId="
+ URLEncoder.encode(transactionId, StandardCharsets.UTF_8)))
.build();
}
支付回调可能重复发送,也可能不按顺序到达。支付状态更新和订单创建应放在同一个事务中,或者通过可靠的幂等流程完成。
Android 端查询最终状态
当 WebView 到达你控制的结果页时,触发一次后端查询:
private static final String RESULT_HOST = "www.mydomain.in";
private static final String RESULT_PATH = "/payment/result";
private boolean isPaymentResultUrl(String value) {
if (value == null) {
return false;
}
Uri uri = Uri.parse(value);
return "https".equalsIgnoreCase(uri.getScheme())
&& RESULT_HOST.equalsIgnoreCase(uri.getHost())
&& RESULT_PATH.equals(uri.getPath());
}
在 WebViewClient 中处理:
mWebview.setWebViewClient(new WebViewClient() {
private boolean statusRequested;
@Override
public void onPageFinished(WebView view, String url) {
pd.dismiss();
if (!statusRequested && isPaymentResultUrl(url)) {
statusRequested = true;
queryPaymentStatusFromBackend(tempTxnId);
}
}
});
查询结果必须由后端返回:
private void queryPaymentStatusFromBackend(String transactionId) {
paymentApi.getStatus(transactionId).enqueue(new Callback<PaymentStatus>() {
@Override
public void onResponse(
Call<PaymentStatus> call,
Response<PaymentStatus> response) {
if (!response.isSuccessful() || response.body() == null) {
showPaymentPending();
return;
}
PaymentStatus result = response.body();
switch (result.getStatus()) {
case "PAID":
showPaymentSuccess(result.getOrderId());
break;
case "FAILED":
showPaymentFailure();
break;
case "PENDING":
default:
showPaymentPending();
break;
}
}
@Override
public void onFailure(Call<PaymentStatus> call, Throwable error) {
showStatusCheckError();
}
});
}
回调处理与 WebView 跳转之间可能存在时间差,因此第一次查询可能返回 PENDING。App 可以在有限时间内按递增间隔重试,但最终结果仍以后端记录为准。
注意事项
- 回调 URL、状态查询接口和支付页面都应使用 HTTPS。
- 必须验证签名、商户号、金额、币种和交易编号。
- 最好再通过支付网关的服务端查询接口确认结果。
- 不要将 URL 中的
success、页面文字或 JavaScript 返回值作为支付凭证。 - 创建订单必须采用幂等设计,避免重复回调或 App 重试生成重复订单。
- 不要把密钥、签名密钥或网关服务端凭证放入 APK。
- 用户关闭 WebView 不应影响支付入账。即使 App 没有运行,后端也应能独立处理回调。
- 如果网关同时提供浏览器返回地址和服务端通知地址,应分别配置。前者用于展示结果,后者用于确认交易。
备注:内容仅供参考。
