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。加载支付页面前,可以启用 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() 重试同一笔交易。
备注:内容仅供参考。
