APP 中使用 WebView 打开 H5 无法调起支付宝支付
结论
浏览器能调起支付宝,但 APP 内的 WebView 无法调起,问题通常不在 form 表单,而在于支付页面提交后跳转到了 alipays://、intent:// 等非 HTTP(S) 地址。系统浏览器可以处理这些外部应用协议,WebView 默认不会自动把它们交给支付宝客户端。
处理时,需要在 WebView 的导航回调中拦截支付宝协议,再通过系统 API 唤起支付宝。正式的支付业务更适合在 APP 中接入支付宝原生支付 SDK。服务端负责创建订单,并向 APP 返回签名后的支付参数。
原因分析
H5 返回的表单通常会自动提交到支付宝收银台,例如:
<form id="alipaysubmit" method="post" action="https://...">
<!-- 支付参数 -->
</form>
<script>
document.getElementById('alipaysubmit').submit();
</script>
表单提交后,支付宝页面可能继续尝试打开支付宝客户端。普通浏览器能够识别并处理外部协议,但 WebView 可能遇到这些问题:
- 没有支持或处理
alipays://、intent://等协议; - 把非 HTTP(S) 地址当作普通网页加载,最终报错;
- JavaScript、DOM Storage 或表单跳转被禁用;
- Android WebView 只处理了旧版
shouldOverrideUrlLoading回调; - iOS 的
WKWebView没有在导航代理中调用系统打开接口; - 设备没有安装支付宝,页面也没有提供浏览器支付的降级路径;
- 支付完成后的回跳地址配置不正确。
支付宝产品类型、接入方式和支付页面实现都会影响实际使用的跳转协议,因此应以 WebView 拦截到的 URL 为准。
Android WebView 处理方式
先启用支付页面通常依赖的基础能力:
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.javaScriptCanOpenWindowsAutomatically = true
然后在 WebViewClient 中拦截导航请求:
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return handleUrl(view.context, request.url.toString())
}
@Suppress("DEPRECATION")
override fun shouldOverrideUrlLoading(
view: WebView,
url: String
): Boolean {
return handleUrl(view.context, url)
}
}
private fun handleUrl(context: Context, url: String): Boolean {
return when {
url.startsWith("http://") || url.startsWith("https://") -> {
false
}
url.startsWith("alipays://") || url.startsWith("alipay://") -> {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
// 未安装支付宝,可提示用户或跳转到安全的降级页面
}
true
}
url.startsWith("intent://") -> {
try {
val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
// 限制可启动的目标,避免任意 Intent 被恶意页面利用
if (intent.`package` == "com.eg.android.AlipayGphone") {
context.startActivity(intent)
} else {
handleFallbackUrl(context, intent)
}
} catch (e: Exception) {
// 记录异常并提示用户,避免 WebView 直接加载 intent://
}
true
}
else -> {
// 不要无条件打开所有未知协议
true
}
}
}
private fun handleFallbackUrl(context: Context, intent: Intent) {
val fallbackUrl = intent.getStringExtra("browser_fallback_url") ?: return
val uri = Uri.parse(fallbackUrl)
if (uri.scheme == "https") {
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
}
}
如果页面使用了其他跳转协议,应先通过日志确认实际 URL,再把可信的协议加入白名单。不要直接将任意非 HTTP(S) 地址传给 startActivity()。
有些页面会通过新窗口打开支付地址,此时还要处理 WebChromeClient.onCreateWindow()。是否需要处理,应根据页面的实际行为判断:
webView.webChromeClient = object : WebChromeClient() {
override fun onCreateWindow(
view: WebView,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message
): Boolean {
val transport = resultMsg.obj as WebView.WebViewTransport
val childWebView = WebView(view.context)
childWebView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
return handleUrl(view.context, request.url.toString())
}
}
transport.webView = childWebView
resultMsg.sendToTarget()
return true
}
}
iOS WKWebView 处理方式
在 WKNavigationDelegate 中识别非 HTTP(S) 协议,并将其交给系统处理:
import UIKit
import WebKit
final class PaymentViewController: UIViewController, WKNavigationDelegate {
private lazy var webView: WKWebView = {
let configuration = WKWebViewConfiguration()
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = self
return webView
}()
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url,
let scheme = url.scheme?.lowercased() else {
decisionHandler(.cancel)
return
}
if scheme == "http" || scheme == "https" {
decisionHandler(.allow)
return
}
if scheme == "alipays" || scheme == "alipay" {
UIApplication.shared.open(url, options: [:]) { success in
if !success {
// 未安装支付宝或系统无法处理该地址
}
}
decisionHandler(.cancel)
return
}
// 未列入白名单的协议不应直接打开
decisionHandler(.cancel)
}
}
如果需要先调用 canOpenURL(_:) 判断是否安装了支付宝,还可能需要在 Info.plist 的 LSApplicationQueriesSchemes 中声明要查询的 Scheme:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>alipays</string>
<string>alipay</string>
</array>
具体需要声明哪些 Scheme,应以当前支付流程产生的跳转地址和支付宝接入文档为准,不要照着示例批量添加。
更稳妥的方案:接入原生支付 SDK
如果 H5 页面只在自有 APP 中使用,建议不要依赖 WebView 内的表单链路调起支付。更稳定的流程如下:
- APP 向业务服务端提交订单信息。
- 服务端调用支付宝接口创建订单。
- 服务端生成签名,并将完整的支付参数返回 APP。
- APP 调用支付宝原生 SDK 发起支付。
- APP 接收 SDK 返回结果,并据此显示界面提示。
- 服务端根据支付宝异步通知更新订单状态。
- APP 再向服务端查询最终支付结果。
客户端返回的 “支付成功” 只能用于交互提示,不能作为订单入账依据。订单的最终状态必须以服务端验签后的异步通知或主动查询结果为准。
排查步骤
可以按以下顺序排查:
- 在 WebView 的导航回调中记录跳转 URL,确认失败时是否出现
alipays://或intent://。 - 确认设备已经安装支付宝,并使用真机测试。
- 确认 WebView 已启用 JavaScript 和 DOM Storage。
- 检查支付页面是否通过新窗口打开地址。
- 检查 WebView 是否错误加载了非 HTTP(S) URL。
- 检查支付宝客户端返回 APP 时使用的回跳 Scheme、Universal Link 或业务回跳页面。
- 检查服务端异步通知、签名验证和订单状态更新是否正常。
- 如果只有部分 Android 或 iOS 版本失败,分别记录系统版本、WebView 版本和实际跳转 URL,再根据差异处理。
注意事项
- 只允许打开明确可信的支付宝协议和目标包名,防止任意 Scheme 或 Intent 注入。
- 不要在客户端生成私钥签名,也不要把支付宝应用私钥放进 H5 或 APP。
- 支付参数应由服务端生成,并通过 HTTPS 传输。
- 不要手工拼接或重复编码支付 URL,以免破坏签名参数。
- 不建议通过关闭证书校验或放宽所有网络安全限制来解决加载问题。
- Android 的
intent://地址可能包含降级 URL,使用前必须校验协议和域名。 - 如果需要兼容未安装支付宝的设备,应提供经过验证的浏览器支付方式或安装提示,不要让 WebView 直接加载未知协议。
- WebView 拦截只能解决支付宝客户端无法打开的问题。支付回调、订单验签和状态同步仍需分别正确实现。
备注:内容仅供参考。
