支付链接应该在 Webview 还是外部浏览器中打开?
结论
如果支付服务商提供官方 Android SDK,优先使用 SDK。只有支付链接时,通常应通过 Android Custom Tabs 或外部浏览器打开,不建议直接嵌入普通 WebView。
只有支付服务商明确支持嵌入式 WebView,而且应用能够正确处理跳转、回调、安全策略和生命周期时,才适合使用 WebView。
为什么不建议直接使用 WebView
支付页面通常不只是普通网页,还可能涉及:
- 跳转到银行或第三方支付 App
- 短信验证码、3-D Secure 等身份验证
- Cookie、JavaScript、重定向和弹窗
intent://、自定义 Scheme 或 App Link- 下载支付凭证或唤起其他应用
- 支付服务商对嵌入式浏览器的安全限制
普通 WebView 对这些流程的兼容性通常不如系统浏览器。有些支付服务商也会明确禁止或不支持用户在 WebView 中完成付款。
外部浏览器的安全边界更清楚,但用户会暂时离开应用。Custom Tabs 是一种折中方案,由浏览器负责渲染,兼容性较好,同时能提供接近应用内页面的体验。
推荐的选择顺序
1. 优先接入官方支付 SDK
如果支付服务商提供 Android SDK,应按照其接入文档实现。SDK 通常已经处理了以下问题:
- 唤起支付 App
- 授权和签名
- 支付结果回调
- 异常状态
- 风控与兼容性
不能只根据客户端页面的跳转结果判断支付是否成功。最终支付状态应由服务端通过支付平台的异步通知或订单查询接口确认。
2. 只有支付 URL 时使用 Custom Tabs
添加 AndroidX Browser 依赖。具体版本应结合项目现有依赖和官方文档选择:
dependencies {
implementation("androidx.browser:browser:<version>")
}
打开支付链接:
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
fun openPaymentUrl(paymentUrl: String) {
val uri = Uri.parse(paymentUrl)
require(uri.scheme == "https") {
"Payment URL must use HTTPS"
}
CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
.launchUrl(this, uri)
}
如果支付链接来自服务端,还要校验主机名,防止应用打开被篡改的地址:
fun isAllowedPaymentUrl(url: String): Boolean {
val uri = Uri.parse(url)
return uri.scheme == "https" &&
uri.host in setOf(
"pay.example.com",
"checkout.example.com"
)
}
允许的域名应以支付服务商实际提供的域名为准,不要直接照搬示例。
3. Custom Tabs 不可用时回退到外部浏览器
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.widget.Toast
fun openPaymentInBrowser(paymentUrl: String) {
val uri = Uri.parse(paymentUrl)
if (uri.scheme != "https") {
Toast.makeText(this, "无效的支付链接", Toast.LENGTH_SHORT).show()
return
}
val intent = Intent(Intent.ACTION_VIEW, uri)
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(this, "未找到可打开支付页面的浏览器", Toast.LENGTH_SHORT).show()
}
}
如何返回应用
支付结束后,可以让服务端跳转到应用的 HTTPS App Link。支付服务商支持时,也可以使用自定义 Scheme。
例如,在 AndroidManifest.xml 中配置 App Link:
<activity
android:name=".PaymentResultActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="example.com"
android:pathPrefix="/payment/result" />
</intent-filter>
</activity>
接收返回参数:
class PaymentResultActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val uri = intent?.data
val orderId = uri?.getQueryParameter("order_id")
if (orderId != null) {
queryPaymentStatusFromServer(orderId)
}
finish()
}
private fun queryPaymentStatusFromServer(orderId: String) {
// 调用自己的服务端接口查询订单状态
}
}
回调 URL 中的 status=success、result=paid 等参数可能被伪造,不能作为最终支付凭证。客户端收到回跳后,应根据订单号向自己的服务端查询真实的支付状态。
如果服务商明确要求使用 WebView
只有支付服务商确认支持时才能使用。应用至少需要限制可访问的域名、拒绝非 HTTPS 页面,并处理特殊 URL Scheme:
import android.content.Intent
import android.net.Uri
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
webView.settings.javaScriptEnabled = true
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val uri = request.url
if (uri.scheme == "https" &&
uri.host in setOf("pay.example.com", "checkout.example.com")
) {
return false
}
return try {
startActivity(Intent(Intent.ACTION_VIEW, uri))
true
} catch (e: Exception) {
true
}
}
}
webView.loadUrl(paymentUrl)
采用这种实现也不能保证所有支付流程都能正常运行。是否启用 JavaScript、第三方 Cookie、多窗口或文件选择,必须遵循支付服务商的文档,不能为了让页面能够打开就无条件放宽设置。
注意事项
- 支付链接必须使用可信的
HTTPS地址,并校验域名。 - 不要忽略 TLS 证书错误,也不要在
onReceivedSslError()中调用handler.proceed()。 - 不要把支付密钥、签名私钥或其他服务端凭据放进 Android 应用。
- 支付 URL 最好由服务端根据订单动态生成,并设置有效期以及与订单的绑定关系。
- 不要根据页面标题、跳转地址或客户端回调参数直接认定支付成功。
- 应防止用户重复点击支付按钮,服务端也要保证订单支付接口具备幂等性。
- 如果应用通过应用商店分发并销售数字内容,还要核对相应商店的应用内购买政策。实体商品、线下服务和数字内容适用的规则可能不同。
选择方式可以简化为:有官方 SDK 就使用 SDK;只有支付链接时优先使用 Custom Tabs;Custom Tabs 不可用时调用外部浏览器;只有支付服务商明确支持时才使用 WebView。
备注:内容仅供参考。
