华为旧款设备首次通过webview调用支付宝H5支付失败
结论
在 WebView 中统一拦截支付宝支付跳转使用的自定义协议,再通过 Android 原生 Intent 唤起支付宝 App。alipays://、intent:// 等非 HTTP URL 不应继续交给 WebView 加载。
部分较早的华为设备或旧版 WebView 在首次处理自定义协议跳转时并不稳定,可能忽略或拦截请求,也可能将其当作普通网页加载。第二次支付能够正常唤起支付宝,往往是因为页面状态、缓存或重定向链已经建立。这个问题应通过完善跳转处理逻辑来解决,不能依赖用户再次发起支付。
原因分析
用户在支付宝 H5 支付页面确认付款后,页面通常会尝试跳转到支付宝 App 对应的自定义协议,例如:
alipays://platformapi/startApp?...
有些场景下也会出现:
intent://...
WebView 主要用于加载 http:// 和 https:// 页面。非 HTTP 协议能否正常跳转,会受到以下因素影响:
- 设备内置 WebView 的版本和厂商定制行为;
- 应用是否实现了
WebViewClient.shouldOverrideUrlLoading(); - 应用是否兼容了新旧两个
shouldOverrideUrlLoading()重载; - 支付链接是否经过多次重定向;
- 页面是否通过新窗口、表单提交或 JavaScript 发起跳转;
- 应用是否错误地将
alipays://URL 再次交给 WebView 加载。
如果代码只实现了其中一个回调,旧设备可能不会进入预期的拦截逻辑。
解决步骤
1. 为 WebView 设置自定义 WebViewClient
同时实现新旧两个 shouldOverrideUrlLoading() 方法,以兼容不同的 Android 和 WebView 版本。
2. 由原生代码处理支付宝协议
检测到 alipays:// 后,使用 Intent.ACTION_VIEW 打开 URI,并返回 true,表示应用已经处理了这次跳转。
3. 处理 intent 协议
遇到 intent:// URL 时,使用 Intent.parseUri() 解析。如果支付宝未安装或目标 Activity 不存在,应捕获异常并提示用户,必要时再处理可信的降级地址。
4. 普通网页仍由 WebView 加载
遇到 http:// 和 https:// 地址时返回 false。不要重复调用 loadUrl(),否则可能导致重定向异常。
Kotlin 示例
@SuppressLint("SetJavaScriptEnabled")
private fun initPayWebView(webView: WebView) {
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
}
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return handlePayUrl(view.context, request.url.toString())
}
@Suppress("DEPRECATION")
override fun shouldOverrideUrlLoading(
view: WebView,
url: String
): Boolean {
return handlePayUrl(view.context, url)
}
}
}
private fun handlePayUrl(context: Context, url: String): Boolean {
return when {
url.startsWith("alipays://", ignoreCase = true) -> {
openExternalApp(context, Uri.parse(url))
true
}
url.startsWith("intent://", ignoreCase = true) -> {
openIntentUrl(context, url)
true
}
url.startsWith("http://", ignoreCase = true) ||
url.startsWith("https://", ignoreCase = true) -> {
false
}
else -> {
// 未知协议不直接放行,避免任意应用被恶意页面唤起。
true
}
}
}
private fun openExternalApp(context: Context, uri: Uri) {
try {
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
if (context !is Activity) {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, "未安装支付宝或无法打开支付宝", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e("AliPayWebView", "Failed to open Alipay: $uri", e)
Toast.makeText(context, "暂时无法打开支付宝", Toast.LENGTH_SHORT).show()
}
}
private fun openIntentUrl(context: Context, url: String) {
try {
val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
component = null
selector = null
if (context !is Activity) {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, "未找到可处理支付请求的应用", Toast.LENGTH_SHORT).show()
} catch (e: Exception) {
Log.e("AliPayWebView", "Failed to handle intent URL", e)
Toast.makeText(context, "支付请求无法打开", Toast.LENGTH_SHORT).show()
}
}
Java 示例
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setDomStorageEnabled(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(
WebView view,
WebResourceRequest request
) {
return handleUrl(view.getContext(), request.getUrl().toString());
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return handleUrl(view.getContext(), url);
}
});
private boolean handleUrl(Context context, String url) {
if (url.startsWith("alipays://")) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.addCategory(Intent.CATEGORY_BROWSABLE);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "未安装支付宝或无法打开支付宝",
Toast.LENGTH_SHORT).show();
}
return true;
}
if (url.startsWith("intent://")) {
try {
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
intent.setSelector(null);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
} catch (Exception e) {
Toast.makeText(context, "支付请求无法打开",
Toast.LENGTH_SHORT).show();
}
return true;
}
return !(url.startsWith("http://") || url.startsWith("https://"));
}
首次仍然无法唤起时如何排查
记录首次和第二次支付时的完整跳转信息:
Log.d("AliPayWebView", "redirect url=$url")
排查时主要确认以下几点:
- 首次支付是否进入了
shouldOverrideUrlLoading(); - 实际使用的是
alipays://、intent://,还是其他中间地址; - 回调中是否再次调用了
webView.loadUrl(url); - 是否抛出了
ActivityNotFoundException或URISyntaxException; - 支付页是否通过新窗口打开。
如果支付页使用 window.open(),还要根据业务需要配置多窗口处理:
webView.settings.setSupportMultipleWindows(true)
webView.webChromeClient = object : WebChromeClient() {
override fun onCreateWindow(
view: WebView,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message
): Boolean {
// 应根据实际支付页面的跳转方式创建临时 WebView,
// 或将可信 URL 转交给当前 WebView。
return false
}
}
在没有确认页面行为前,不要直接返回 true,否则可能已经创建了窗口,却没有继续加载支付地址。
注意事项
- 必须同时覆盖两个
shouldOverrideUrlLoading()方法,因为旧设备可能只调用旧签名。 - 成功拦截后必须返回
true,否则 WebView 仍会尝试加载自定义协议。 - 普通 HTTP/HTTPS 地址通常应返回
false,不要无条件再次调用loadUrl()。 - 不要清理支付过程中需要使用的 Cookie、缓存或页面状态。
- 只允许可信协议和可信支付来源,防止网页通过任意 URI 唤起其他应用。
- 不要将完整支付 URL、授权参数或订单信息写入生产日志。
- 支付结果应以服务端异步通知和订单查询结果为准,不能只根据支付宝 App 是否成功唤起、页面回跳结果或前端提示来判断。
- 如果首次跳转完全没有进入任何 URL 拦截回调,需要结合真实支付页面排查表单提交、新窗口和 WebView 内核行为,不能仅凭设备品牌认定唯一原因。
备注:内容仅供参考。
