查看原文
其他

干货 | 关于Apple Pay接入和开发,看这一篇就够了

杨伟 携程技术中心 2019-05-02

作者简介

 

杨伟,携程金融支付中心支付Native iOS组Leader,目前主要负责中文版携程APP和国际版携程APP的支付项目功能开发和团队管理。热爱生活,喜欢探索。 


Apple Pay 是苹果 2014 年发布的 iOS8 开始支持的一项新功能,2016 年 iOS9 开始支持中国银联卡,Apple Pay 凭借其支付安全,简洁快速的用户体验,开发接入简单,在国内市场占据了一席之地。


携程是最早一批接入Apple Pay 的中国应用商户,目前携程国际版 APP 中也支持 Apple Pay 支付。 国际版的 Apple Pay 功能还在不断扩展中,支持接入新的国家币种和支付通道。


本篇主要从 iOS 前端客户端的角度出发,对 Apple Pay 的应用内接入和开发中遇到的一些问题,做一些总结和回顾,希望给开发人员带来启发和收获。


文章将主要包含三部分:


一是Apple Pay 支付流程梳理,其中客户端、服务端、苹果、支付供应商分别担任什么样的角色;


二是客户端如何支持 Apple Pay:开发证书怎么配置,开发注意事项;


三是国内与国际 Apple Pay 接入的一些区别。


一、Apple Pay 整体流程

下图为目前国内 Apple Pay 支付接入的一个通用的流程(银联API模式),仅供参考:     

  


整个流程中如下:


1、客户端通过苹果API,在 APP 应用内展示 Apple Pay 支付控件。


2、用户在 Apple Pay 的支付控件上进行生物验证(指纹或者人脸识别)或者手机密码验证。


3、苹果在用户验证通过之后,会生成一个用户选中的银行卡相关的 PaymentToken 加密数据,Apple Pay 必须在有网情况下才能进行,苹果需要从开发者网站上使用证书的公钥进行加密,完成后通过 API 回调返回给客户端前端。


4、客户端获取到 PaymentToken 后,给服务端发送扣款请求,等待支付结果。


5、服务端收到客户端上送的 PaymentToken,解密 PaymentToken 取出一些关键字段信息,附带其他订单信息,再与支付供应商(如国内银联)进行通信发起扣款。


6、服务端收到扣款结果后,再返支付结果给手机客户端,最终通知用户支付结果。


二、苹果如何保证安全性


1、Apple Pay 绑卡


在 iOS 手机的 Wallet 应用中,用户可以绑定一张真实的银行卡。在绑定过程中,需要输入银行卡的安全字段,还需要进行手机号验证。


绑定成功后,我们可以在手机 Wallet 里面查看这张卡,在卡详情里面会看到一个设备卡号。 这个设备卡号就是银行卡的虚拟卡号。这个虚拟卡号也就是苹果下发的 PaymentToken 里面解密后可以拿到的卡号,这个卡号最后要发给支付供应商(如国内银联)做扣款用的。


虚拟卡号不会固定,每次银行卡重新绑定都会重新生成,发生变化。这样就就确保之后所有Apple Pay支付过程中网络中不会传输真实的银行卡号,增强安全性。


苹果在绑卡过程中会与不同的发卡机构通信和交互,我们在 Apple Pay 刚进入国内时会看到有的银行卡可以绑定成功,有的不能绑定成功,就是因为有些通道还不稳定。


另外,我们可以在 Apple Pay 的开发者网站上找到特殊的 API ,通过这些 API 某些发卡机构的 APP 就可以直接在应用内向Wallet里面绑卡,如某些银行的 APP 就可以做到,这些功能需要专门向苹果提交申请,拿到专门的授权文件,配置到开发工程,才能调通那些 API。相关 API 都可以在网上查到,这里仅供了解,我们一般的 APP 开发用不到。


Apple Pay 的整个平台,苹果在与发卡机构、支付供应商、客户端前端各个流程都有着一整套安全机制。我们可以看出苹果在 Apple Pay 上的投入以及重视程度。 面向 iOS 客户端的 Apple Pay API,也是做到了简单和接入方便,具体使用可以在苹果开发者网站上查询到。


2、Apple Pay 数据加密


Apple Pay 中传输中的 PaymentToken 有着一套非常完善的加密安全机制。


国外统一用的是 ECC 加密方式,只有中国用的是 RSA 加密方式,详见官方PaymentToken说明。


PaymentToken数据是 JSON 数据格式,其中包含了苹果加密后的支付信息数据。


PaymentToken 示例


ECC 就是椭圆加密算法,是一种非对称加密方式。


ECC 加密方式,PaymentToken 格式样例:


{
 "data":"H22hDxsaFP8JZCuLf6AYu99IB2rLvRbQaZ/NtuQB/vS++ctYSCqPYWUH69eCV59eUdpEcHSOnJPX+95FpFuyRxryb+xzG0EIO0T7fPwDHPyZcA1gixgG/DD10oQgbTf6uWPWkB3z5E3ENsl4QKOTOpladzj4cLKPerlb28s9gjuJtXSylxbnSe5aRFBFwdqsus39hfXNdlIqRCJzYLiGrIFThxoKSv7kVK32u5UYjjAizMkH5tE0fvFbPjGvi3JLrGCozmJDytFWQUVjzba6ORLtd+ui0oe2PXzkVun8w0BNK2tsKh/Jfh4HdK/JD/TBbFLz06SkVa0T7OSjuyItln7NNh3PC+sAltllUyAtjYnY3X76t/DnJ23GVbwZ4t1bUjieZ2loBg+j1w0adcB0dR9UA1Eh1fexrdZW737sd6wp6Edd+PoEW4G5hY0RM/F280q6SD/wWwr80rOkQmE=",
  "header": {    

"ephemeralPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBZITXjr/xw6fsaVQ2bHCJVwglJt9f9ikgFGtsCKun1Spbei/xi756rt/aTA13ugqNiI2ITk0QvivqV6PhwmrHQ==",
   "publicKeyHash":"KjwaMOiXIeoSDyLEz5TDPl3uXAVcwZ2f5/P/jGH+gMo=",
"transactionId": "2e9d75661d1641d188d4e646901fe708836b5a5ac7c705c15915b27e23786488"
  },
 "signature":"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIID4jCCA4igAwIBAgIIJEPyqAad9XcwCgYIKoZIzj0EAwIwejEuMCwGA1UEAwwlQXBwbGUgQXBwbGljYXRpb24gSW50ZWdyYXRpb24gQ0EgLSBHMzEmMCQGA1UECwwdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTE0MDkyNTIyMDYxMVoXDTE5MDkyNDIyMDYxMVowXzElMCMGA1UEAwwcZWNjLXNtcC1icm9rZXItc2lnbl9VQzQtUFJPRDEUSBBcHBsaWNhdGlvbiBJbnRlZ3JhdGlvbiBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMCCCRD8qgGnfV3MA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTUwNDA1MDQwOTE2WjAvBgkqhkiG9w0BCQQxIgQgmDfQVEEPlInbB9tQ/AQjni+COkvegoWmfwtpbwe7cuUwCgYIKoZIzj0EAwIERzBFAiB/4NnlUFbNkEqdJ6OGRtA78mZ30539P226OwsNRptycgIhAIzFEYsHH2xuMLVfxEiUmtQxcMdqgW1BgtL/AygaVaAhAAAAAAAA",
  "version":"EC_v1"
}


解密 ECC 加密的 data 后看到:


{
   "applicationExpirationDate": "190131",
   "applicationPrimaryAccountNumber":"370295XXXXX5435",
    "currencyCode":"840",
   "deviceManufacturerIdentifier": "XXXXXXXXXX",
    "paymentData":{        "emvData":"nycBgJ82AgDCnyYIG2vuQydGkMafEAcGhgEDoLABXzQBAJUFgAABAACCAhzAnwMGAAAAAAAAnxoCCECaAxQQBJwBAJ83BLnvab4="
    },
   "paymentDataType": "EMV",
   "transactionAmount": 100
}


国内 RSA 加密方式,得到的 PaymentToken 格式:


{"data" :"juTLgE+0gQPg3RSe5DdJj1/w4amEvJOWSIka+nHNGFmFVd028omhkwMNNqG0exHgT39DAXnKqVNBh4ExGvIEgO7yi97JhDOmwq0nTyvPF/U393wgizrmoe8zX1FpUzI7e2co8PIYCJLkC6uTIuuumsE//503nDhvnz9frzmiMYVhdquf56couB028QBhJiQAupLM+NawVJ41i7e7WJIfyVhYEEn1Qw0TKZKy+Y65PkhAgdwlUGkKUI6r2IpHCc/l4EWvpn1tcVQvVeoG6qJxUdszgL6qrJBLtaT+/8teg9/jfn0iQwipEgYfTjalgHwXx3nop0dK2ZxzcOzdclfTY3uguM6HBNK6rK3hL2B/LnidAuWE0EWMp5/kqNunDJsSXsUM4+g6zg7ceVjlndZ0YLrQozxRkhPkgHHGtjFxn01PpeGSMMdoAVc8iOgpGKrjx/AAIp2jNY1fZZ1G4cYzW+gsJo5PrmPV7gGxA+M+HG5xdwMVfa9/cxS0qVU2eMw92OB7hdgaIW6cwONETgrd0Y2vF/oHiw7AbRiX/3GxkOJfJ9/5S0P6cxJF4JsvVHkrgit6gZPMCMXFQxDQsK8DlmQ9YxzDRIJCb9jxKE0=",
    "header" : {
"publicKeyHash": "YcmdtTV209LCGVZV99T0lNab3yo0KnudAyBKQPo+5e8=",
       "transactionId":"41a6eb8dd8602def5f41463ad314e484289258df71ab17b50204ea4795a3db52",
"wrappedKey" :"l6w4oBmvLF/f/6Gj7idhO5aIFlwZ5qZrqSLxR+mLqsjJqHLf2OUTObn+UOO/Iaupc+nc6Kuz1ZTQbBMG2w6/KC8F07lZTPnCOcHVxxBP03UQn6gNkNV8DLNNqJ9GoBDzbGy/dWgKBBPNEdgA59jKY+8H/XQzvNuZqiFDM4LTr+O3/FdqGy6PxfFHRwRNV15WoKtsfQ91xPf++MI4GoTZSdxpc63ewm/8l5Q81AqnZMH03PMRyu8POT92tl10tg8GRmQFCXMgBm7GM+6nz0tebOoLndspqHbe9xtAGmxzseuFCi4A2q8WaqzPgnQ917bvuUnxNHAkXoPTVIUCsXzxXA=="
    },
"signature":"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIIEtTCCBFugAwIBAgIIEVmL4CjCUF8wCgYIKoZIzj0EAwIwejEuMCwGA1UEAwwlQXBwbGUgQXBwbGljYXRpb24gSW50ZWdyYXRpb24gQ0EgLSBHMzEmMCQGA1UECwwdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTE0MTIwMzAxMDAyNloXDTE5MTIwMjAxMDAyNlowZzEtMCsGA1UEAwwkZWNjLXNtcC1icm9rZXItc2lnbl9VQzQtUFJPRF9LcnlwQ8XDTE2MTEwMjEyMzg0OFowLwYJKoZIhvcNAQkEMSIEIP+1yZ0K5+XsvQRPOcudpBElpvl64hJ2pE0PryWNhFSlMA0GCSqGSIb3DQEBAQUABIIBAHuOnvaNlq/1oBGLGyi/+6BmUJLAZjtArsHnpMArgEcmW8qy1oObVeSF7puBjhsnnoWsGnSr+c0/jxYgxKk10RuzLdxdITbx2DJab+3AzQ4gCatfKuzlzPAdqYaumZLnr/6pJ+CPLaTlWZJZhrPY7GNiqdN2v22tXZQae6fHLwXXqNzjdVOsHNYRRReZA9jbQ46haaI/3HpYuQpRhLu5EAkReaYZZKuOhpyxxUeatFtRFwo/vH3yOU5eG07VASCGB33SZqgjJNMZWACIF3FRKK9W8yL50ywEZzDCpCV3/6Jk9yZB1h/jU7qu3iTBXo8jU6lGRcoKjl0DzkiIEQVHB+kAAAAAAAA=", 

"version" :"RSA_v1"


中国区的 Apple Pay 加密方式是其实是 RSA + AES,RSA 是一种非对称加密,AES 是一种对称加密。上面的data 实际由 AES 加密后得到。AES 的密钥藏在 Header 里面的 wrappedKey 中,被 RSA 加密保护着。


国内加密data 解密后,如下:


{
      "applicationPrimaryAccountNumber":"62583300888880215",
      "applicationExpirationDate": "270101",
      "currencyCode": "156",
      "transactionAmount": 0,
      "deviceManufacturerIdentifier": "062010011111",
      "paymentDataType": "EMV",
      "paymentData": {
             "emvData":"nyYItis3L6CiQbufNgIACYECAE2DgZCgujJqvZh6gtCOicVyx2tOh1ncXHOQ9bhYMObxz+IHR5a4PD93thtwu7RKyIFb2zab3wkj0oMcra5Cf+J+JbXdk0FxxxxxxxxxxT56HVqNMBp4M/7Uh36lblsiLkvW0H3rwLVWE/CV4/h0="
       }
}


解密后我们都可以看到的 deviceManufacturerIdentifier 就是手机 Wallet 里面绑定银行卡的虚拟卡号,这个是要给到支付供应商发起扣款用到的。


三、支付供应商

支付供应商主要是提供支付扣款渠道的商户,比如中国的银联,国外的Adyen,可以参考苹果支持的支付供应商。支付供应商主要负责与发卡机构交互,发起扣款。


四、客户端开发 Apple Pay

客户端开发的工作主要包括: 


1、证书的准备; 

2、APP工程配置和证书使用;

3、API 调用;


证书准备


开发者需要在苹果开发者网站上提交 Apple Pay 的证书。


生成Apple Pay 证书首先需要准备一个 CSR 文件,通过 mac 电脑的钥匙串应用可以快速生成,这个 CSR 文件就是一个文本文件CertificateSigningRequest.certSigningRequest。


需要将这个 CSR 文件上传到苹果开发者网站上:



做 iOS 开发比较了解,苹果的各种开发证书都是通过这种形式生成。


生成 Apple Pay 证书的 CSR 文件时注意: 中国区生成 CSR 时需要使用 RSA 加密方式。 非中国区生成 CSR 时需要使用 ECC 加密方式。


CSR 中包含公钥信息,生成同时,mac 电脑钥匙串应用里会生成对应的私钥。 上传 CSR 到苹果开发者网站生成证书完成后,下载安装到 mac 上,可以在钥匙串应用里面看到私钥,并可以导出私钥,用于解密。


ApplePay 的证书生成流程官方和网上都有流程介绍,这里不再赘述。


工程配置和证书使用


在苹果开发者账号网站上可以看到,每一个 Apple Pay 证书都对应和关联一个 MerchantId,每一个 Apple Pay 证书在实际使用过程中,对应一套密钥,对应一套支付扣款通道。苹果下发的 PaymentToken 加密时,就是根据 APP 调用 API 时传入的 MerchantId 找到对应的公钥进行加密处理。


客户端调用ApplePay API 时,需要指定证书的 MerchantId,建议不要客户端写死在本地,最好由服务端下发,做成灵活可配置的,这样可以进行支付通道切换。


证书生成之后,需要在 Xcode 工程文件的 Targets 的 capability 里,打开 Apple Pay的开关,并勾选证书对应的 MerchantId,否则打出的APP 安装包 ipa 无法唤起 Apple Pay 的支付页面。


开启选项的位置,如图所示:



Apple Pay 证书有时效性,一般两年后会过期,需要更换密钥。证书过期的处理,具体操作可以在苹果开发者网站上执行。


在下图所示页面更换证书时,先绑定新的 CSR 生成新证书,Activate 之后,线上就会切换到新证书模式了。



线上验证过证书没有问题,之后就可以 Revoke 旧证书。


证书更新过程中,线上 APP 客户端不需要调整,证书的密钥都配置在服务端,为了不影响用户支付流程,APP的服务端需要控制下,在证书更换期间,先短暂关闭 Apple Pay 支付方式,待证书新密钥生效后再开启。


五、国内与国际ApplePay接入的一些区别和注意事项

携程支付最早接入国内银联的 Apple Pay 渠道时,采用的是银联 SDK 的模式。


这种 SDK 模式就是中国银联以对外提供 SDK 的方式给到 APP 使用Apple Pay 支付。 具体的 SDK 和使用说明都在可以在中国银联开发网站上下载到。 


使用 SDK 的好处就是客户端接入简单,只管调用 SDK 的接口,处理支付结果回调即可,客户端不需要处理各种异常。银联 SDK 调用时需要传递一个 tn 号,这个 tn 号由银联生成,这个 tn 号对应到一笔交易,APP 调用银联 SDK 时,必须传递。


使用 SDK 模式的缺陷是:


1、对 APP 打包的 ipa 安装文件影响比较大,对于对 size 要求比较高的携程 APP 来说,当时银联 SDK 的 size 在支付这边占了不少比例。原因应该是银联的 SDK,内部本身又包含了自己的通信等其他框架。


2、当整个 APP 要求进行 Https,或者 ipv6 等类似特殊支持时,APP 对银联 SDK 的依赖比较重,需要与银联方沟通和确认,确保银联 SDK 支持。 


3、SDK 模式下,证书和密钥都是由银联生成,APP 开发用银联提供的 CSR 文件生成 Apple Pay 证书并绑定,证书和密钥更新麻烦。


4、Apple Pay 的页面展示完全由银联 SDK 控制,当需要增加展示项时,需要向外部寻求银联 SDK 的支持。


后来,携程支付改造接入方式,使用 API 模式,不再使用 SDK 方式接入银联 Apple Pay。 这种方式,对于接入商户来说,证书和密钥都由接入商户自己管理,不再依赖支付供应商,客户端和服务端开发更加灵活。


这种方式,iOS 开发者需要自己控制和处理 Apple Pay 的 UI 展示和交互,并应对以下的一些异常:


1、部分场景中,用户验证通过后,正在发送扣款请求时,用户又点击了取消按钮,取消 Apple Pay 操作,在这种场景下,支付需要采用一定的方案和策略避免多扣用户的钱。


2、同其他支付方式一样,要考虑如何处理异常情况下的订单重复提交问题。


更多的安全校验:在实际项目中,解密 Apple Pay 的数据后,并没有看到金额,我们尝试直接给到用户随机优惠,提交给银联服务端的支付金额与 APP 中展示给用户看到的金额不一致时,发现无法扣款成功,由此可以断定,苹果与支付供应商之间一定存在着金额校验机制。APP 端是无法越过用户进行任意金额扣款的,必须在 Apple Pay的页面上展示明确的金额。


在后续的国际 Apple Pay 接入中,携程分别接入了 Adyen 和 SoftBank Payment Service,这两个渠道需要的 Apple Pay证书都是 ECC 加密方式的,与国内银联不同。 


另外 Adyen 接入时,证书由Adyen 生成,加解密都在 Adyen 处理。 而 SoftBank 的证书生成和加解密又都需要携程支付自己完成,所以携程支付在获取的 PaymentToken 之后需要针对不同的支付通道做不同的处理。


在国际版Apple Pay 实际调用时,APP 服务端根据不同的币种,使用不同的支付通道, Apple Pay 的 API 支持指定证书的 MerchantId,进入支付时,APP客户端根据服务端下发的 MerchantId 去调用 Apple Pay。通过这种方式就能实现,不同币种,支付使用不同的证书通道。


除了证书通道做成服务下发可配置之外,国际版的 Apple Pay 产品业务比中文版更加复杂,不同的业务场景,需要支持的银行通道又不一样。通过类似的方式,APP根据服务端控制下发的卡通道,如 Visa、MasterCard 等支付方式限制,来控制 Apple Pay 的支持和展示。


六、总结

在 iOS 开发中,接入 Apple Pay 不仅仅是简单的 API 调用和展示,需要考虑用户的一些行为和交互,任何支付流程都一样,要为用户的体验和财产负责。要充分考虑各种可能存在的异常,如何避免和解决各种异常,需要从整体上做更加全面的设计。


Apple Pay 的正常运作需要客户端、服务端、发卡机构、支付供应商以及苹果各个环节紧密合作。通过参与 Apple Pay 开发以及对 Apple Pay 安全的不断深入了解,会发现苹果确实很注重细节问题,有很多值得借鉴和学习的思路和设计。在支付流程中,如何保证安全同时给用户做到极致的简单体验,苹果确实做到了。


【推荐阅读】




    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存