Android 支付时重定向到 Browser 并返回交易 ID
结论
可以实现”应用发起支付 → 浏览器打开银行页面 → 支付后返回应用”这一流程。不过,Android 应用通常无法直接关闭外部 Browser 的 Activity,也不能把浏览器当作普通 Activity,通过 startActivityForResult() 获取交易结果。
建议采用以下流程:
- App 向商户服务端请求创建支付订单。
- 使用浏览器或 Chrome Custom Tabs 打开银行返回的支付 URL。
- 用户完成支付后,银行跳转到商户的 HTTPS 回调地址。
- 回调地址通过 Android App Link 唤起 Checkout Activity。
- App 根据订单号向服务端查询并确认支付结果。
交易 ID 和支付状态必须由服务端向银行或支付网关验证,不能直接相信回跳 URL 中的参数。
推荐的流程
Checkout Activity
↓ 创建支付订单
商户服务端
↓ 返回 paymentUrl 和 paymentSessionId
Browser / Custom Tabs
↓ 用户完成付款
银行支付页面
↓ 跳转到商户 HTTPS returnUrl
Android App Link
↓ 唤起应用
Checkout Activity
↓ 使用 paymentSessionId 查询
商户服务端
↓ 向银行验签或查询订单
返回可信的支付状态和 transactionId
这里有两种不同的回调:
- 前端返回地址:负责把用户从浏览器带回 App。
- 服务端通知地址:由银行主动向商户服务端发送支付结果,通常称为 webhook、notify URL 或 server callback。
前端回跳只处理页面导航。确认交易结果需要依靠服务端通知或主动查单。
打开银行支付页面
可以通过 ACTION_VIEW 使用普通浏览器打开支付页面:
fun openPaymentPage(paymentUrl: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(paymentUrl))
startActivity(intent)
}
如果支付机构允许,通常可以选择 Chrome Custom Tabs。页面和证书仍由浏览器处理,但视觉上与 App 的衔接会更自然:
fun openPaymentPage(context: Context, paymentUrl: String) {
val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
customTabsIntent.launchUrl(context, Uri.parse(paymentUrl))
}
项目需要添加相应的 AndroidX Browser 依赖。具体版本应根据现有的依赖管理策略选择,不必盲目锁定某个版本。
不要为了调用 finish(),随意把支付页面放进 WebView。部分银行禁止在 WebView 中完成支付,而且 WebView 在 Cookie、页面跳转、3-D Secure、外部身份验证和安全策略等方面更容易遇到兼容问题。应优先遵循银行或支付网关提供的 SDK 和接入规范。
使用 Android App Link 返回应用
假设商户控制的域名是:
https://shop.example.com/payment/return
可以在 AndroidManifest.xml 中为 Checkout Activity 配置 App Link:
<activity
android:name=".CheckoutActivity"
android:exported="true"
android:launchMode="singleTask">
<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="shop.example.com"
android:pathPrefix="/payment/return" />
</intent-filter>
</activity>
使用 android:autoVerify="true" 时,需要在相应域名下正确部署 Digital Asset Links 文件:
https://shop.example.com/.well-known/assetlinks.json
该文件需要声明,允许使用对应 application ID 和签名证书的应用处理这个域名。如果调试版和正式版使用不同证书,需要分别配置对应的 SHA-256 证书指纹。
如果银行只允许配置 HTTPS 返回地址,可以直接将 App Link URL 设为支付返回地址。也可以让银行先跳转到商户服务端页面,再由该页面引导用户返回 App。
在 Checkout Activity 中接收回跳
Checkout Activity 可能仍在任务栈中,因此建议同时处理首次启动和 onNewIntent():
class CheckoutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_checkout)
handlePaymentReturn(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handlePaymentReturn(intent)
}
private fun handlePaymentReturn(intent: Intent?) {
if (intent?.action != Intent.ACTION_VIEW) {
return
}
val uri = intent.data ?: return
if (uri.scheme != "https" ||
uri.host != "shop.example.com" ||
uri.path != "/payment/return"
) {
return
}
val paymentSessionId =
uri.getQueryParameter("payment_session_id") ?: return
queryPaymentResult(paymentSessionId)
}
private fun queryPaymentResult(paymentSessionId: String) {
// 调用商户服务端接口查询支付状态。
// 不要根据回跳 URL 直接显示“支付成功”。
}
}
回跳地址可以采用以下形式:
https://shop.example.com/payment/return?payment_session_id=ps_abc123
更稳妥的方式是只在 URL 中携带一次性的支付会话标识,不要直接携带能够决定业务结果的交易状态。
为什么不能直接从浏览器返回 Activity Result
下面这种方式不适用于通用的浏览器支付流程:
startActivityForResult(browserIntent, REQUEST_PAYMENT)
外部浏览器不了解应用定义的结果协议,也不会在银行支付完成后主动执行:
setResult(...)
finish()
换成 Activity Result API 也无法改变这一限制。它适用于由应用控制,或者明确支持结果返回协议的 Activity,不适用于任意浏览器中的网页跳转。
网页需要通过 App Link、Deep Link 或支付机构提供的 Android SDK 唤起 App。
能否关闭 Browser Activity
应用无法可靠地对第三方浏览器 Activity 调用 finish()。浏览器属于另一个应用,也可能拥有独立的任务栈。
触发 App Link 后,Android 会把 Checkout Activity 带到前台。用户看到的效果通常是离开浏览器并返回 App,但浏览器进程及其页面并没有因此真正关闭。
singleTask、FLAG_ACTIVITY_CLEAR_TOP 等任务栈配置可以帮助复用已有的 Checkout Activity,但无法保证关闭所有浏览器,也不应依赖某个浏览器的特定任务栈行为。
如果业务必须完全控制页面关闭,只能考虑支付机构明确支持的 SDK 或受控页面容器。能否使用 WebView,仍需遵守支付机构的安全要求。
安全返回交易结果
不要把下面这种回跳地址作为最终的支付依据:
myshop://payment/result?success=true&transactionId=123456
其他应用或用户都可能构造类似的 Intent。如果 App 仅根据这些参数判断支付成功,支付结果就可能被伪造。
建议这样处理:
- App 向服务端创建订单,由服务端生成不可预测的
paymentSessionId。 - 服务端保存用户、订单和支付会话之间的绑定关系。
- 银行完成支付后,将结果通知商户服务端。
- 服务端验证签名、金额、币种、商户号、订单号和支付状态。
- App 返回前台时,只携带
paymentSessionId或订单引用。 - App 使用当前登录用户的身份凭据,向自己的服务端查询结果。
- 服务端确认订单属于当前用户后,再返回可信的
transactionId和支付状态。
例如,App 最终查询自己的接口:
GET /api/payments/ps_abc123
Authorization: Bearer <access-token>
服务端返回:
{
"orderId": "order_10001",
"status": "PAID",
"transactionId": "bank_tx_987654"
}
只有经过服务端验证的结果,才能用于发货、增加余额或更新订单状态。
还需要注意的问题
- 优先使用经过域名验证的 HTTPS App Link。自定义 Scheme,例如
myshop://,可能被其他应用注册或拦截。 - 回调中的
state或支付会话标识必须随机且不可预测,并与当前用户和订单绑定。 - 服务端的支付处理必须具有幂等性,因为银行通知、用户回跳和主动查询可能重复发生。
- 用户可能在支付后手动关闭浏览器,所以不能只依赖前端回跳。Checkout 页面应支持重新查询订单状态。
- 用户回跳时,服务端通知可能还没有到达。App 可以进行短时间轮询,但需要设置合理的次数和超时时间,并向用户显示”支付结果确认中”。
- 不要把商户密钥、签名私钥或银行接口密钥放进 Android App。
- 不能只检查
transactionId是否存在,还需要核对金额、币种、订单号、商户号和最终支付状态。 - 如果银行或支付网关提供官方 Android SDK,应按照其协议处理回调、签名和 3-D Secure 流程。
这个流程在 Android 上可以实现。重点不是关闭浏览器并获取 Activity Result,而是通过经过验证的链接把用户带回应用,再由服务端确认真实的支付结果。
备注:内容仅供参考。
