Skip to content
关于 博客 学习 探索 模式 Case studies
本页内容
  • 第 1 步:让商家发现您的支付应用
  • 第 2 步:让商家知道客户是否有可以付款的注册工具
    • AndroidManifest.xml
    • AIDL
    • 实现IsReadyToPayService
    • 参数
    • 响应
    • 权限
  • 第 3 步:让客户付款
    • AndroidManifest.xml
    • 参数
    • 响应
    • 权限
  • 第 4 步:验证调用者的签名证书
  • Home
  • All articles

Android 支付应用开发者指南

了解如何调整 Android 支付应用以与 Web Payments 配合使用,并为客户提供更好的用户体验。

May 25, 2020
Available in: English、Español、Português和한국어
Appears in: Web 付款
Yuichi Araki
Yuichi Araki
TwitterGitHub
Eiji Kitamura
Eiji Kitamura
TwitterGitHubHomepage
本页内容
  • 第 1 步:让商家发现您的支付应用
  • 第 2 步:让商家知道客户是否有可以付款的注册工具
    • AndroidManifest.xml
    • AIDL
    • 实现IsReadyToPayService
    • 参数
    • 响应
    • 权限
  • 第 3 步:让客户付款
    • AndroidManifest.xml
    • 参数
    • 响应
    • 权限
  • 第 4 步:验证调用者的签名证书

Payment Request API 为 web 带来了一个基于浏览器的内置界面,允许用户比以往更轻松地输入所需的支付信息。该 API 还可以调用特定于平台的支付应用。

使用 Web Payments 的特定于平台的 Google 支付应用的结账流程。

与仅使用 Android Intents 相比,Web Payments 可以更好地与浏览器、安全性和用户体验集成:

  • 在商家网站的上下文中,支付应用作为模式启动。
  • 实现是对现有支付应用的有益补充,使您能够充分利用用户群。
  • 检查支付应用的签名以防止旁加载。
  • 支付应用可以支持多种支付方式。
  • 可以集成任意支付方式,例如加密货币、银行转账等。 Android 设备上的支付应用甚至可以集成需要访问设备硬件芯片的支付方法。
要了解商家如何与支付应用集成,请查看支付交易的生命周期。

在 Android 支付应用中实现 Web Payments 需要四个步骤:

  1. 让商家发现您的支付应用。
  2. 让商家知道客户是否有可以付款的注册工具(例如信用卡)。
  3. 让客户付款。
  4. 验证调用者的签名证书。

要查看 Web Payments 的实际效果,请查看 android-web-payment 演示。

第 1 步:让商家发现您的支付应用 #

为了让商家使用您的支付应用,他们需要使用 Payment Request API 并使用支付方式标识符指定您支持的支付方式。

如果您的支付应用使用唯一的付款方式标识符,那么您可以设置自己的付款方式清单,以便浏览器可以发现您的应用。

要了解详细发现过程的工作原理以及如何设置新的付款方式,请查看设置付款方式。

第 2 步:让商家知道客户是否有可以付款的注册工具 #

商户可以调用hasEnrolledInstrument()查询客户是否可以付款。您可以将IS_READY_TO_PAY作为 Android 服务实现来回答此查询。

AndroidManifest.xml #

使用带有org.chromium.intent.action.IS_READY_TO_PAY操作的意图过滤器声明您的服务。

<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服务是可选项。如果支付应用中没有此类意图处理程序,那么 Web 浏览器会假定该应用始终可以进行支付。

AIDL #

IS_READY_TO_PAY服务的 API 在 AIDL 中定义。创建两个 AIDL 文件,其内容如下:

app/src/main/aidl/org/chromium/IsReadyToPayServiceCallback.aidl

package org.chromium;
interface IsReadyToPayServiceCallback {
oneway void handleIsReadyToPay(boolean isReadyToPay);
}

app/src/main/aidl/org/chromium/IsReadyToPayService.aidl

package org.chromium;
import org.chromium.IsReadyToPayServiceCallback;

interface IsReadyToPayService {
oneway void isReadyToPay(IsReadyToPayServiceCallback callback);
}

实现IsReadyToPayService #

下面是IsReadyToPayService的最简单实现示例:

class SampleIsReadyToPayService : Service() {
private val binder = object : IsReadyToPayService.Stub() {
override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
callback?.handleIsReadyToPay(true)
}
}

override fun onBind(intent: Intent?): IBinder? {
return binder
}
}

参数 #

请将以下参数作为 Intent extras 传递给onBind:

  • methodNames
  • methodData
  • topLevelOrigin
  • topLevelCertificateChain
  • topLevelCertificateChain
  • paymentRequestOrigin
override fun onBind(intent: Intent?): IBinder? {
val extras: Bundle? = intent?.extras
// …
}

methodNames #

被查询的方法的名称。元素是methodData字典中的键,表明了支付应用支持的方法。

val methodNames: List<String>? = extras.getStringArrayList("methodNames")

methodData #

methodNames中每一项到methodData的映射。

val methodData: Bundle? = extras.getBundle("methodData")

topLevelOrigin #

没有方案的商户来源(顶级浏览上下文的无方案来源)。例如,https://mystore.com/checkout将作为mystore.com传递。

val topLevelOrigin: String? = extras.getString("topLevelOrigin")

topLevelCertificateChain #

商家的证书链(顶级浏览上下文的证书链)。对于本地主机和磁盘上的文件(均是没有 SSL 证书的安全上下文),要设为 null。证书链是必要的,因为支付应用可能对不同网站有不同的信任要求。

val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")

每个Parcelable都是一个带有"certificate"键和字节数组值的Bundle

val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p as Bundle).getByteArray("certificate")
}

paymentRequestOrigin #

在 JavaScript 中调用new PaymentRequest(methodData, details, options)构造函数的 iframe 浏览上下文的无方案起源。如果该构造函数是从顶级上下文调用的,那么此参数的值等于topLevelOrigin参数的值。

val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")

响应 #

该服务可以通过handleIsReadyToPay(Boolean)方法发送它的响应。

callback?.handleIsReadyToPay(true)

权限 #

您可以使用Binder.getCallingUid()来检查调用方是谁。请注意,您必须在isReadyToPay方法、而不是在onBind方法中执行此操作。

override fun isReadyToPay(callback: IsReadyToPayServiceCallback?) {
try {
val callingPackage = packageManager.getNameForUid(Binder.getCallingUid())
// …

有关如何验证调用包是否具有正确签名的信息,请参阅验证调用方的签名证书。

第 3 步:让客户付款 #

商家调用show() 启动支付应用,以便客户进行支付。支付应用通过 Android Intent PAY调用,并在 Intent 参数中包含交易信息。

支付应用使用methodName和details响应,这两个响应是支付应用特定的并且对浏览器不透明。浏览器会通过 JSON 反序列化将details字符串转换为商家的 JavaScript 对象,但除此之外并不会强制执行任何有效性。浏览器不会修改details;该参数的值将直接传递给商家。

AndroidManifest.xml #

使用了PAY意图过滤器的活动应该具有<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://bobpay.xyz/pay" />

<meta-data
android:name="org.chromium.payment_method_names"
android:resource="@array/method_names" />

</activity>

resource必须是字符串列表,每个字符串都必须是一个有效的绝对 URL,使用 HTTPS 方案,如下所示。

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="method_names">
<item>https://alicepay.com/put/optional/path/here</item>
<item>https://charliepay.com/put/optional/path/here</item>
</string-array>
</resources>

参数 #

以下参数将作为 Intent extras 传递给活动:

  • methodNames
  • methodData
  • topLevelOrigin
  • topLevelCertificateChain
  • paymentRequestOrigin
  • total
  • modifiers
  • paymentRequestId
val extras: Bundle? = intent?.extras

methodNames #

正在使用的方法的名称。这些元素是methodData字典中的键。这些是支付应用支持的方法。

val methodNames: List<String>? = extras.getStringArrayList("methodNames")

methodData #

从每个methodNames到methodData的映射。

val methodData: Bundle? = extras.getBundle("methodData")

merchantName #

商家结帐页面(浏览器的顶级浏览上下文)<title> HTML 标记的内容。

val merchantName: String? = extras.getString("merchantName")

topLevelOrigin #

没有方案的商家来源(顶级浏览上下文的无方案来源)。例如, https://mystore.com/checkout会作为mystore.com传递。

val topLevelOrigin: String? = extras.getString("topLevelOrigin")

topLevelCertificateChain #

商家的证书链(顶级浏览上下文的证书链)。对于本地主机和磁盘上的文件(均是没有 SSL 证书的安全上下文),要设为 null。每个Parcelable都是一个带有certificate键和字节数组值的 Bundle。

val topLevelCertificateChain: Array<Parcelable>? =
extras.getParcelableArray("topLevelCertificateChain")
val list: List<ByteArray>? = topLevelCertificateChain?.mapNotNull { p ->
(p as Bundle).getByteArray("certificate")
}

paymentRequestOrigin #

在 JavaScript 中调用new PaymentRequest(methodData, details, options)构造函数的 iframe 浏览上下文的无方案起源。如果该构造函数是从顶级上下文调用的,那么此参数的值等于topLevelOrigin参数的值。

val paymentRequestOrigin: String? = extras.getString("paymentRequestOrigin")

total #

表示交易总额的 JSON 字符串。

val total: String? = extras.getString("total")

这是字符串的示例内容:

{"currency":"USD","value":"25.00"}

modifiers #

JSON.stringify(details.modifiers)的输出,其中details.modifiers仅包含supportedMethods和total 。

paymentRequestId #

“推送支付”应用程序应与交易状态关联的PaymentRequest.id。商家网站将使用此字段查询“推送支付”应用的带外交易状态。

val paymentRequestId: String? = extras.getString("paymentRequestId")

响应 #

活动可以通过带有RESULT_OK的setResult发回它的响应。

setResult(Activity.RESULT_OK, Intent().apply {
putExtra("methodName", "https://bobpay.xyz/pay")
putExtra("details", "{\"token\": \"put-some-data-here\"}")
})
finish()

您必须将两个参数指定为 Intent extras:

  • methodName:正在使用的方法的名称。
  • details:包含商家用于完成交易所需的 JSON 字符串。如果交易成功,那么字符串是true,接着details的构建方式需要保证JSON.parse(details)会成功。

如果交易未在支付应用中完成,您可以传递RESULT_CANCELED,例如,如果用户未能在支付应用中输入正确的帐户 PIN 码。浏览器可以让用户选择不同的支付应用。

setResult(RESULT_CANCELED)
finish()

如果从被调用的支付应用收到的支付响应的活动结果设置为RESULT_OK,那么 Chrome 将检查非空的methodName和附加信息中的details。如果验证失败,Chrome 将从request.show()返回一个被拒绝的 promise,并带有以下开发人员将看到的的错误消息之一:

'Payment app returned invalid response. Missing field "details".'
'Payment app returned invalid response. Missing field "methodName".'

权限 #

活动可以使用它的getCallingPackage()方法检查调用者。

val caller: String? = callingPackage

最后一步是验证调用者的签名证书,以确认调用包具有正确的签名。

第 4 步:验证调用者的签名证书 #

您可以通过IS_READY_TO_PAY中的Binder.getCallingUid(),以及PAY中的Activity.getCallingPackage()来检查调用者的包名称。为了确认调用者是您心目中的浏览器,应该检查其签名证书并确保它与正确的值匹配。

如果您的目标是 API 级别 28 及更高级别,并且正在与具有单个签名证书的浏览器集成,则可以使用PackageManager.hasSigningCertificate() 。

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
)

PackageManager.hasSigningCertificate()是单证书浏览器的首选,因为它可以正确处理证书轮换。(Chrome 具有单个签名证书。)具有多个签名证书的应用无法轮换它们。

如果您需要支持较旧的 API 级别 27 级更低级,或者需要处理具有多个签名证书的浏览器,则可以使用PackageManager.GET_SIGNATURES 。

val packageName: String = … // The caller's package name
val certificates: Set<ByteArray> = … // The correct set of signing certificates

val packageInfo = getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
val sha256 = MessageDigest.getInstance("SHA-256")
val signatures = packageInfo.signatures.map { sha256.digest(it.toByteArray()) }
val verified = signatures.size == certificates.size &&
signatures.all { s -> certificates.any { it.contentEquals(s) } }
付款
Last updated: May 25, 2020 — Improve article
Return to all articles
分享
订阅

Contribute

  • 提交错误
  • 查看源代码

相关内容

  • developer.chrome.com
  • Chrome 动态
  • 案例研究
  • 播客
  • 节目

连接

  • Twitter
  • YouTube
  • Google Developers
  • Chrome
  • Firebase
  • Google Cloud Platform
  • 所有产品
  • 条款和隐私权
  • 社区准则

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 4.0 License, and code samples are licensed under the Apache 2.0 License. For details, see the Google Developers Site Policies.