了解如何调整您的 Android 付款应用以支持 Web Payments,并为客户提供更好的用户体验。
发布时间:2020 年 5 月 5 日;最后更新时间:2025 年 5 月 27 日
Payment Request API 为 Web 带来了基于浏览器的内置界面,让用户可以比以往更轻松地输入所需的付款信息。该 API 还可以调用平台专用付款应用。
与仅使用 Android intent 相比,Web Payments 可更好地与浏览器集成,并提升安全性和用户体验:
- 付款应用会在商家网站的上下文中以模态窗口的形式启动。
- 此实现是对现有付款应用的补充,可让您充分利用现有用户群。
- 系统会检查付款应用的签名,以防止旁加载。
- 付款应用可以支持多种付款方式。
- 您可以集成任何付款方式,例如加密货币、银行转账等。Android 设备上的付款应用甚至可以集成需要访问设备上硬件芯片的方法。
在 Android 付款应用中实现 Web Payments 需要完成以下四个步骤:
- 让商家发现您的付款应用。
- 告知商家客户是否有已注册的付款工具(例如信用卡)可供付款。
- 允许客户付款。
- 验证调用方的签名证书。
如需了解 Web Payments 的实际运作方式,请查看 android-web-payment 演示。
第 1 步:让商家发现您的付款应用
按照设置付款方式中的说明在 Web 应用清单中设置 related_applications 属性。
商家若要使用您的付款应用,则需要使用 PaymentRequest API,并使用付款方式标识符指定您支持的付款方式。
如果您有属于您付款应用的专属付款方式标识符,则可以设置自己的付款方式清单,以便浏览器发现您的应用。
第 2 步:告知商家客户是否有已注册且可以用于付款的付款工具
商家可以调用 hasEnrolledInstrument() 来查询客户是否能够付款。您可以将 IS_READY_TO_PAY 实现为 Android 服务来回答此查询。
AndroidManifest.xml
使用操作为 org.chromium.intent.action.IS_READY_TO_PAY 的 intent 过滤器声明您的服务。
<service android:name=".SampleIsReadyToPayService" android:exported="true"> <intent-filter> <action android:name="org.chromium.intent.action.IS_READY_TO_PAY" /> </intent-filter> </service> IS_READY_TO_PAY 服务是可选的。如果付款应用中没有此类 intent 处理脚本,则网络浏览器会假定该应用始终可以进行付款。
AIDL
IS_READY_TO_PAY 服务的 API 在 AIDL 中定义。创建两个包含以下内容的 AIDL 文件:
org/chromium/IsReadyToPayServiceCallback.aidl
package org.chromium; interface IsReadyToPayServiceCallback { oneway void handleIsReadyToPay(boolean isReadyToPay); } org/chromium/IsReadyToPayService.aidl
package org.chromium; import org.chromium.IsReadyToPayServiceCallback; interface IsReadyToPayService { oneway void isReadyToPay(IsReadyToPayServiceCallback callback, in Bundle parameters); } 实施 IsReadyToPayService
以下示例展示了 IsReadyToPayService 的最简单实现:
Kotlin
class SampleIsReadyToPayService : Service() { private val binder = object : IsReadyToPayService.Stub() { override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) { callback?.handleIsReadyToPay(true) } } override fun onBind(intent: Intent?): IBinder? { return binder } } Java
import org.chromium.IsReadyToPayService; public class SampleIsReadyToPayService extends Service { private final IsReadyToPayService.Stub mBinder = new IsReadyToPayService.Stub() { @Override public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) { if (callback != null) { callback.handleIsReadyToPay(true); } } }; @Override public IBinder onBind(Intent intent) { return mBinder; } } 响应
服务可以使用 handleIsReadyToPay(Boolean) 方法发送响应。
Kotlin
callback?.handleIsReadyToPay(true) Java
if (callback != null) { callback.handleIsReadyToPay(true); } 权限
您可以使用 Binder.getCallingUid() 检查调用方是谁。请注意,您必须在 isReadyToPay 方法(而非 onBind 方法)中执行此操作,因为 Android OS 可以缓存和重复使用服务连接,而这不会触发 onBind() 方法。
Kotlin
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?, parameters: Bundle?) { try { val untrustedPackageName = parameters?.getString("packageName") val actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid()) // ... Java
@Override public void isReadyToPay(IsReadyToPayServiceCallback callback, Bundle parameters) { try { String untrustedPackageName = parameters != null ? parameters.getString("packageName") : null; String[] actualPackageNames = packageManager.getPackagesForUid(Binder.getCallingUid()); // ... 收到进程间通信 (IPC) 调用时,请始终检查 null 的输入参数。这一点尤为重要,因为 Android OS 的不同版本或分支可能会以意外的方式运行,如果不加以处理,可能会导致错误。
虽然 packageManager.getPackagesForUid() 通常会返回单个元素,但您的代码必须处理调用方使用多个软件包名称的不常情况。这可确保您的应用保持稳健。
如需了解如何验证调用软件包是否具有正确的签名,请参阅验证调用方的签名证书。
参数
Chrome 139 中添加了 parameters 软件包。应始终与 null 进行对比检查。
以下参数会传递给 parameters 软件包中的服务:
packageNamemethodNamesmethodDatatopLevelOriginpaymentRequestOrigintopLevelCertificateChain
packageName 已在 Chrome 138 中添加。您必须先根据 Binder.getCallingUid() 验证此参数,然后才能使用其值。此验证至关重要,因为 parameters 软件包由调用方完全控制,而 Binder.getCallingUid() 由 Android OS 控制。
在 WebView 中,以及通常用于本地测试的非 https 网站(例如 http://localhost)上,topLevelCertificateChain 为 null。
第 3 步:让客户付款
商家调用 show() 以启动付款应用,以便客户付款。系统会使用 Android intent PAY 调用付款应用,并在 intent 参数中包含交易信息。
付款应用会使用 methodName 和 details 进行响应,这两个值是特定于付款应用的,对浏览器而言是不可见的。浏览器会使用 JSON 字符串反序列化将 details 字符串转换为商家的 JavaScript 字典,但不会强制执行任何其他有效性检查。浏览器不会修改 details;该参数的值会直接传递给商家。
AndroidManifest.xml
具有 PAY intent 过滤器的 activity 应包含一个 <meta-data> 标记,用于标识应用的默认付款方式标识符。
如需支持多种付款方式,请添加包含 <string-array> 资源的 <meta-data> 标记。
<activity android:name=".PaymentActivity" android:theme="@style/Theme.SamplePay.Dialog"> <intent-filter> <action android:name="org.chromium.intent.action.PAY" /> </intent-filter> <meta-data android:name="org.chromium.default_payment_method_name" android:value="https://bobbucks.dev/pay" /> <meta-data android:name="org.chromium.payment_method_names" android:resource="@array/chromium_payment_method_names" /> </activity> android:resource 必须是字符串列表,其中每个字符串都必须是采用 HTTPS 协议方案的有效绝对网址,如下所示。
<?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="chromium_payment_method_names"> <item>https://alicepay.com/put/optional/path/here</item> <item>https://charliepay.com/put/optional/path/here</item> </string-array> </resources> 参数
以下参数会作为 Intent extra 传递给 activity:
methodNamesmethodDatamerchantNametopLevelOrigintopLevelCertificateChainpaymentRequestOrigintotalmodifierspaymentRequestIdpaymentOptionsshippingOptions
Kotlin
val extras: Bundle? = getIntent()?.extras Java
Bundle extras = getIntent() != null ? getIntent().getExtras() : null; methodNames
所使用的方法的名称。这些元素是 methodData 字典中的键。这些是付款应用支持的方法。
Kotlin
val methodNames: List<String>? = extras.getStringArrayList("methodNames") Java
List<String> methodNames = extras.getStringArrayList("methodNames"); methodData
将每个 methodNames 映射到 methodData。
Kotlin
val methodData: Bundle? = extras.getBundle("methodData") Java
Bundle methodData = extras.getBundle("methodData"); merchantName
商家结账页(浏览器的顶级浏览上下文)的 <title> HTML 标记的内容。
Kotlin
val merchantName: String? = extras.getString("merchantName") Java
String merchantName = extras.getString("merchantName"); topLevelOrigin
不带 scheme 的商家来源(顶级浏览上下文的无 scheme 来源)。例如,https://mystore.com/checkout 会作为 mystore.com 传递。
Kotlin
val topLevelOrigin: String? = extras.getString("topLevelOrigin") Java
String topLevelOrigin = extras.getString("topLevelOrigin"); topLevelCertificateChain
商家的证书链(顶级浏览上下文的证书链)。对于 WebView、localhost 或磁盘上的文件,该值为 null。每个 Parcelable 都是一个包含 certificate 键和字节数组值的 Bundle。
Kotlin
val topLevelCertificateChain: Array<Parcelable>? = extras.getParcelableArray("topLevelCertificateChain") val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p -> (p as Bundle).getByteArray("certificate") } Java
Parcelable[] topLevelCertificateChain = extras.getParcelableArray("topLevelCertificateChain"); if (topLevelCertificateChain != null) { for (Parcelable p : topLevelCertificateChain) { if (p != null && p instanceof Bundle) { ((Bundle) p).getByteArray("certificate"); } } } paymentRequestOrigin
调用了 JavaScript 中的 new PaymentRequest(methodData, details, options) 构造函数的 iframe 浏览上下文的无架构来源。如果从顶级上下文调用了构造函数,则此参数的值等于 topLevelOrigin 参数的值。
Kotlin
val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin") Java
String paymentRequestOrigin = extras.getString("paymentRequestOrigin"); total
表示交易总金额的 JSON 字符串。
Kotlin
val total: String? = extras.getString("total") Java
String total = extras.getString("total"); 下面是字符串的内容示例:
{"currency":"USD","value":"25.00"} modifiers
JSON.stringify(details.modifiers) 的输出,其中 details.modifiers 仅包含 supportedMethods、data 和 total。
paymentRequestId
“推送付款”应用应与交易状态关联的 PaymentRequest.id 字段。商家网站将使用此字段来查询“推送付款”应用,以便在非正规渠道中了解交易状态。
Kotlin
val paymentRequestId: String? = extras.getString("paymentRequestId") Java
String paymentRequestId = extras.getString("paymentRequestId"); 响应
activity 可以使用 RESULT_OK 通过 setResult 发回其响应。
Kotlin
setResult(Activity.RESULT_OK, Intent().apply { putExtra("methodName", "https://bobbucks.dev/pay") putExtra("details", "{\"token\": \"put-some-data-here\"}") }) finish() Java
Intent result = new Intent(); Bundle extras = new Bundle(); extras.putString("methodName", "https://bobbucks.dev/pay"); extras.putString("details", "{\"token\": \"put-some-data-here\"}"); result.putExtras(extras); setResult(Activity.RESULT_OK, result); finish(); 您必须将两个参数指定为 intent extra:
methodName:所用方法的名称。details:包含商家完成交易所需信息的 JSON 字符串。如果成功为true,则必须以使JSON.parse(details)成功的方式构建details。如果不需要返回任何数据,则此字符串可以是"{}",商家网站将会将其作为空 JavaScript 字典接收。
如果交易未在付款应用中完成(例如,用户未在付款应用中输入正确的账号 PIN 码),您可以传递 RESULT_CANCELED。浏览器可能会允许用户选择其他付款应用。
Kotlin
setResult(Activity.RESULT_CANCELED) finish() Java
setResult(Activity.RESULT_CANCELED); finish(); 如果从调用的付款应用收到的付款响应的 activity 结果设为 RESULT_OK,则 Chrome 会检查其 extras 中是否有非空 methodName 和 details。如果验证失败,Chrome 将从 request.show() 返回一个被拒绝的 promise,并显示以下面向开发者的错误消息之一:
'Payment app returned invalid response. Missing field "details".' 'Payment app returned invalid response. Missing field "methodName".' 权限
activity 可以使用其 getCallingPackage() 方法检查调用方。
Kotlin
val caller: String? = callingPackage Java
String caller = getCallingPackage(); 最后一步是验证调用方的签名证书,以确认调用软件包具有正确的签名。
第 4 步:验证调用方的签名证书
您可以在 IS_READY_TO_PAY 中使用 Binder.getCallingUid() 检查调用方的软件包名称,在 PAY 中使用 Activity.getCallingPackage() 进行检查。为了实际验证调用方是否为您预期的浏览器,您应检查其签名证书,并确保其与正确的值匹配。
如果您以 API 级别 28 及更高级别为目标平台,并且要与具有单个签名证书的浏览器集成,则可以使用 PackageManager.hasSigningCertificate()。
Kotlin
val packageName: String = … // The caller's package name val certificate: ByteArray = … // The correct signing certificate val verified = packageManager.hasSigningCertificate( callingPackage, certificate, PackageManager.CERT_INPUT_SHA256 ) Java
String packageName = … // The caller's package name byte[] certificate = … // The correct signing certificate boolean verified = packageManager.hasSigningCertificate( callingPackage, certificate, PackageManager.CERT_INPUT_SHA256); 对于单证书浏览器,PackageManager.hasSigningCertificate() 是首选,因为它可以正确处理证书轮替。(Chrome 只有一个签名证书。)具有多个签名证书的应用无法轮替这些证书。
如果您需要支持 API 级别 27 及更低级别,或者需要处理具有多个签名证书的浏览器,则可以使用 PackageManager.GET_SIGNATURES。
Kotlin
val packageName: String = … // The caller's package name val expected: Set<String> = … // The correct set of signing certificates val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES) val sha256 = MessageDigest.getInstance("SHA-256") val actual = packageInfo.signatures.map { SerializeByteArrayToString(sha256.digest(it.toByteArray())) } val verified = actual.equals(expected) Java
String packageName = … // The caller's package name Set<String> expected = … // The correct set of signing certificates PackageInfo packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); Set<String> actual = new HashSet<>(); for (Signature it : packageInfo.signatures) { actual.add(SerializeByteArrayToString(sha256.digest(it.toByteArray()))); } boolean verified = actual.equals(expected); 调试
使用以下命令查看错误或信息消息:
adb logcat | grep -i pay