Android WebView 如何获取第三方支付按钮的 onclick 回调?
结论
不要直接监听第三方按钮的 onclick。在 Android WebView 中,更稳妥的方式是监听页面跳转,识别支付网关约定的回调 URL,再通知原生应用。
如果页面和按钮代码都由你控制,可以使用 addJavascriptInterface() 建立 JavaScript 与 Android 之间的通信。对于无法修改的第三方支付页面,不建议通过注入 JavaScript 劫持按钮事件。这种做法容易受到跨域限制、页面结构调整和安全策略的影响,还可能违反支付网关的接入要求。
推荐方案:拦截支付回调 URL
先在支付网关后台或下单参数中配置返回地址,例如:
https://example.com/payment/result
再通过 WebViewClient.shouldOverrideUrlLoading() 判断当前页面是否跳转到了该地址:
class PaymentActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this)
setContentView(webView)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
// 避免把 iframe、图片等子资源误判为支付回调
if (!request.isForMainFrame) {
return false
}
return handlePaymentUrl(request.url)
}
@Suppress("DEPRECATION")
override fun shouldOverrideUrlLoading(
view: WebView,
url: String
): Boolean {
return handlePaymentUrl(Uri.parse(url))
}
}
webView.loadUrl("https://payment.example.com/checkout")
}
private fun handlePaymentUrl(uri: Uri): Boolean {
val isPaymentCallback =
uri.scheme == "https" &&
uri.host == "example.com" &&
uri.path == "/payment/result"
if (!isPaymentCallback) {
return false
}
val orderId = uri.getQueryParameter("order_id")
val status = uri.getQueryParameter("status")
onPaymentPageReturned(orderId, status)
return true
}
private fun onPaymentPageReturned(orderId: String?, status: String?) {
// 这里只表示 WebView 已返回,不代表支付结果可信。
// 应使用 orderId 请求业务服务器查询最终支付状态。
}
}
返回 true 表示应用已经处理该 URL,WebView 不再继续加载。普通网页地址应返回 false。
如果支付网关支持自定义 URL Scheme,也可以配置类似下面的地址:
myapp://payment/callback?order_id=123
对应的拦截逻辑可以写成:
private fun handlePaymentUrl(uri: Uri): Boolean {
if (uri.scheme != "myapp" ||
uri.host != "payment" ||
uri.path != "/callback"
) {
return false
}
val orderId = uri.getQueryParameter("order_id")
onPaymentPageReturned(orderId, null)
return true
}
回调地址能采用什么格式,取决于支付网关是否允许配置返回 URL,Android 端不能单方面决定。
页面由自己控制时:使用 JavaScript Bridge
如果 Submit 按钮在你自己的页面中,可以在用户点击时调用 Android 暴露的方法:
class PaymentBridge(
private val onSubmit: (String) -> Unit
) {
@JavascriptInterface
fun onSubmit(orderId: String) {
onSubmit.invoke(orderId)
}
}
注册通信接口:
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(
PaymentBridge { orderId ->
runOnUiThread {
// 收到页面提交事件
}
},
"AndroidPayment"
)
页面中的 JavaScript:
document.querySelector('#submitButton').addEventListener('click', function () {
if (window.AndroidPayment) {
window.AndroidPayment.onSubmit(currentOrderId);
}
});
这种方式只能说明用户点击了按钮,不能证明付款已经成功。用户可能重复点击按钮,也可能取消支付,支付还可能失败或仍在处理中。
为什么不建议直接注入 onclick
使用 evaluateJavascript() 给第三方按钮追加监听器虽然简单,但并不可靠:
webView.evaluateJavascript(
"""
document.querySelector('#submitButton')?.addEventListener('click', function () {
AndroidPayment.onSubmit('');
});
""".trimIndent(),
null
)
主要问题包括:
- 第三方页面可能使用
iframe,外层页面无法直接访问跨域内容。 - 按钮可能动态生成,注入代码执行时对应元素还不存在。
- 第三方一旦修改 DOM、按钮 ID 或提交流程,代码就会失效。
- CSP、重定向或原生表单提交都可能改变页面行为。
- 向不受信任的页面暴露
addJavascriptInterface()会增加安全风险。
JavaScript 注入只适合完全由自己控制的页面,不能作为第三方支付结果的回调方案。
支付结果必须由服务器确认
WebView 跳转、JavaScript 回调和 URL 参数都可能被伪造。应用收到回调后,应将订单号发送给自己的服务器查询:
WebView 返回应用
↓
应用把 orderId 发送给业务服务器
↓
业务服务器查询支付平台,或读取支付平台的异步通知结果
↓
服务器返回最终订单状态
发货、充值、开通会员等操作,只能依据服务端验签后的异步通知,或支付平台服务端接口返回的查询结果。
其他注意事项
- 优先使用支付网关提供的 Android SDK。如果官方要求使用浏览器、Custom Tabs 或外部支付应用,应按其接入流程处理。
- 校验回调 URL 时,必须同时检查
scheme、host和path,不能只用contains("success")判断。 - 不要通过 URL 传递支付密钥、令牌或完整的敏感信息。
addJavascriptInterface()只能向使用 HTTPS 的可信页面开放,并且只暴露必要的方法。- 页面显示“支付成功”不代表服务端订单已经支付。还需要处理失败、取消、超时、重复回调和支付处理中等状态。
- 如果第三方网关没有提供返回 URL 或 SDK,也不允许修改页面,就无法稳定获取按钮原始的
onclick回调。此时需要联系支付服务商,确认其正式支持的移动端回调机制。
备注:内容仅供参考。
