PAYATHON 2026

如何从 Android WebView 获取支付网关的 POST 数据

支付老李

结论

Android WebView 没有可靠的 API 能读取页面导航时提交的原始 POST body。onPageStarted()、onPageFinished() 和 shouldInterceptRequest() 可以观察 URL 或部分请求信息,但无法将支付网关提交的 POST 参数安全、完整地传给 App。

建议按以下流程处理:

  1. 支付网关将结果 POST 到你的 HTTPS 后端。
  2. 后端验证签名,并主动向支付网关查询交易状态。
  3. 后端以幂等方式更新交易并创建订单。
  4. 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 没有运行,后端也应能独立处理回调。
  • 如果网关同时提供浏览器返回地址和服务端通知地址,应分别配置。前者用于展示结果,后者用于确认交易。

备注:内容仅供参考。