PAYATHON 2026

Android WebView 重定向支付网关时交易未授权

支付阿杰

结论

问题不在 WebView 重定向,而在于支付网关拒绝了重复使用的交易请求。

错误信息:

the transaction is already initiated

通常表示该 PaymentID 或 trandata 已被网关使用、正在处理中,或者在首次加载后又被提交了一次。shouldOverrideUrlLoading() 只能决定页面如何打开,无法让已经失效的一次性交易恢复有效。

需要由服务端重新创建支付交易,获取新的支付 URL。同时,WebView 只能加载该 URL 一次,不要在重定向回调中重复调用 loadUrl()。

常见原因

支付网关通常把 PaymentID、加密后的 trandata 或支付会话当作一次性凭证。以下操作都可能触发“交易已经发起”错误:

  • 同一个支付 URL 被加载了两次。
  • 在 shouldOverrideUrlLoading() 中调用了 view.loadUrl(url),同时返回 false,导致 WebView 既手动加载请求,又按默认方式加载请求。
  • Activity 或 Fragment 重建后再次执行 loadUrl()。
  • 用户刷新页面、返回后重新进入,或者代码进行了自动重试。
  • 支付 URL 已在浏览器、Postman 或另一台设备中打开。
  • 服务端重复返回同一个 PaymentID。
  • 首次请求虽然没有显示支付页面,但网关已经把交易状态更新为“已发起”。

因此,只修改 WebView 的重定向处理方式,通常无法让旧的交易凭证重新生效。

解决步骤

1. 重新创建支付交易

不要继续使用示例中的 PaymentID=8501798561070830 及其对应的 trandata。应用服务端应重新调用支付网关的初始化接口,生成新的交易编号、加密数据和支付 URL。

交易能否取消或重新激活,取决于支付网关的接口规则,需要通过网关后台或服务端日志确认。客户端不应自行拼接、修改或复用 PaymentID 和 trandata。

2. 保证 WebView 只加载一次

可以使用 savedInstanceState,避免 Activity 因屏幕旋转等原因重建后再次发起支付:

public class PaymentActivity extends AppCompatActivity {

    private WebView pgWebview;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_payment);

        pgWebview = findViewById(R.id.pgWebview);

        WebSettings settings = pgWebview.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setDomStorageEnabled(true);
        settings.setUseWideViewPort(true);
        settings.setLoadWithOverviewMode(true);

        pgWebview.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(
                    WebView view,
                    WebResourceRequest request
            ) {
                Uri uri = request.getUrl();
                String scheme = uri.getScheme();

                // HTTP/HTTPS 重定向继续交给当前 WebView。
                if ("http".equalsIgnoreCase(scheme)
                        || "https".equalsIgnoreCase(scheme)) {
                    return false;
                }

                return handleExternalScheme(uri);
            }

            @SuppressWarnings("deprecation")
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                Uri uri = Uri.parse(url);
                String scheme = uri.getScheme();

                if ("http".equalsIgnoreCase(scheme)
                        || "https".equalsIgnoreCase(scheme)) {
                    return false;
                }

                return handleExternalScheme(uri);
            }
        });

        if (savedInstanceState == null) {
            pgWebview.loadUrl(webURL);
        } else {
            pgWebview.restoreState(savedInstanceState);
        }
    }

    private boolean handleExternalScheme(Uri uri) {
        try {
            startActivity(new Intent(Intent.ACTION_VIEW, uri));
            return true;
        } catch (ActivityNotFoundException e) {
            return true;
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        pgWebview.saveState(outState);
        super.onSaveInstanceState(outState);
    }
}

如果页面不需要 JavaScript,就不要开启 setJavaScriptEnabled(true)。支付页面是否依赖 JavaScript,应以网关接入文档为准。

3. 正确实现 shouldOverrideUrlLoading()

对于普通的 http 和 https 地址,直接返回 false:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    return false;
}

不要使用下面的写法:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);
    return false;
}

这段代码一边让 WebView 按默认方式处理,一边手动调用 loadUrl(),可能发出重复请求。

如果确实需要手动加载 URL,必须返回 true:

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);
    return true;
}

普通支付页面的重定向一般不需要手动加载。

4. 检查支付初始化是否被重复调用

在客户端和服务端记录以下信息:

  • 支付交易的创建时间。
  • PaymentID,写入日志时需要脱敏。
  • 支付初始化接口的调用次数。
  • WebView 首次执行 loadUrl() 的次数。
  • Activity 或 Fragment 是否发生过重建。
  • 网关返回的交易状态和错误码。

用户首次点击支付按钮后,应暂时禁用按钮,避免连续点击导致重复创建或打开同一笔交易:

payButton.setEnabled(false);
startPayment();

初始化失败后可以恢复按钮。如果支付状态不明确,应先由服务端查询交易状态,不要直接复用原 URL。

部分支付网关依赖 Cookie。加载支付页面前,可以启用 Cookie:

CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setAcceptCookie(true);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    cookieManager.setAcceptThirdPartyCookies(pgWebview, true);
}

Cookie 配置通常用于处理登录状态丢失、循环跳转或会话失效。当前错误已经明确说明交易被发起过,因此应先检查重复加载和一次性交易凭证,不要只调整 Cookie。

不要绕过 SSL 错误

如果代码中的 SSLTolerentWebViewClient 使用下面的方式忽略证书错误:

handler.proceed();

应立即移除。支付流程不能绕过 TLS 证书验证,否则可能泄露支付数据,并带来中间人攻击风险。需要修复服务器证书、证书链、域名匹配或设备时间问题,让 WebView 使用默认验证:

pgWebview.setWebViewClient(new WebViewClient());

证书验证失败时,应终止加载并修复证书配置,不能通过“信任所有证书”来处理。

仍然报错时怎么判断

如果使用全新的服务端交易凭证,并确认 WebView 只调用了一次 loadUrl() 后,页面仍立即显示相同错误,问题很可能出在支付网关或商户服务端,例如:

  • 商户后台重复使用了订单号。
  • 初始化接口已经创建交易,却返回了旧的支付 URL。
  • 网关要求通过 POST 提交,实际使用的却是 GET。
  • 请求参数的签名、有效期或商户配置不符合网关要求。
  • 网关仍把首次失败的尝试标记为“已发起”。

此时,应由服务端查询该订单和 PaymentID 的实际状态,并携带脱敏后的请求时间、商户订单号、网关响应码及交易编号,联系支付网关技术支持。不要刷新页面,也不要反复调用 loadUrl() 重试同一笔交易。

备注:内容仅供参考。