苹果服务器与开发者服务器之间的通讯,包括苹果通知、开发者主动请求苹果服务器、新的验证收据流程等。
- 以前只支持
Apple server
单向发送状态变化的通知给我们自己的服务器,现在支持主动去请求Apple server
获取历史交易记录 -
StoreKit 2
服务器端接口支持了新的格式(JWS
格式)。 - 沙盒环境测试优化
构建开发者服务器可以实现以下几个功能:
- 接收内购状态改变通知
- 通过接口跟踪内购状态变化(获取所有的交易历史记录)
- 随时验证用户的访问权限(是否已购买,是否已退款等信息)
- 跟踪退款信息
一、验证票据
开发者可以用 receipt
收据或者使用StoreKit v2
新的 signed transactiond
来验证订单。
Receipt
收据验证方式:
- 在用户设备App中验证收据
- 在开发者服务端通过苹果
/verifyReceipt
接口验证收据
服务器端在通过/verifyReceipt
接口验证票据时,新 API 的数据结构也发生了变化。例如统一了购买时间、过期时间、原始购买时间格式,新增了 appAcountToken
字段、内购类型字段、退款时间、退款原因、促销优惠类型等。
旧的 receipts
收据内容如下图:
新的 JWS
格式的交易格式内容,如下图。对比 receipts
收据,可以知道有那些变化。旧的有 GMT
(格林威治标准时间)、PST
(太平洋标准时间)、Unix timestamp
(Unix
时间戳),新的格式,只保留了 Unix
时间戳,并且字段做了更新。
内购的类型,也有返回了。
这个就是上面提到的关系的用户信息的 UUID
。这里苹果用 appAccountToken
字段。
这个是用户退款时间和退款原因的字段。从之前的 cancellation_date
改成现在的 revocationData
。
最后是促销优惠的类型。
二、新增了一些 API
内购历史订单查询 API
获取交易历史记录:get_transaction_history
,只需要用户任意一个交易里的originalTransactionId
即可获取所有的历史记录。需要注意的是,这个返回的数据有一个字段 hasMore
为 ture
,表示历史订单有更新。
查找收据发票 API
这个就是前言提到的用户收到苹果的收据发票时,无法与开发者的订单匹配的问题!现有有新的 API 来解决了,这个新的接口,可以让用户提供的发票上的ORDER ID
查到对应的transaction
交易信息。整个流程,主是这样,用户客诉,提供订单 ID,查询到状态,为用户进行补单或者支持服务。开发者服务器记得保存对应的用户订单 ID 做好映射。
查找该客户过去的退款 API
除了苹果服务器的通知退款REFUND
后,开发者现在可以主动通过inApps/v1/history
接口,查询用户的所有交易订单,来确认订单的状态是不是退款(撤销)。
苹果提供了查询所有内购订单的接口,但是不可能让开发者查一次,然后再判断那些是退款订单吧!所以,苹果提供了另一个接口。这个接口也是一样,通过用户的任一个 originalTransactionId
可以查到这个用户的所有退款记录订单。用户退款有一个单独的 refundDate
字段,如果有内容时间就表示是退款。
三、通过通知跟踪状态
回调通知通过notificationType
和subType
两个字段区分,自动续期订阅相关通知如下。
notificationType | subType | 说明 |
---|---|---|
DID_CHANGE_RENEWAL_PREF | DOWNGRADE | 降级,降级下个周期生效 |
DID_CHANGE_RENEWAL_PREF | UPGRADE | 升级,升级马上生效 |
DID_CHANGE_RENEWAL_STATUS | AUTO_RENEW_ENABLED | 开启自动续期 |
...
理解不同类型通知中交易信息的含义。每个通知中都会携带交易信息,位于 data
-> signedTransactionInfo
字段,它总是当时(发送通知那一刻)生效的交易,比如初次购买时,携带的购买成功的交易信息。
总结来说,App Store server notifications V2
提供了多达20多种通知类型!当然,这里变动这么多,苹果不可能在原来的接口直接改啊!所以是有 v1 和 v2 接口,开发者可以设置。最后就是通知返回的内容,多了一个 subtype
子类型,还有对应的 version
为 2 表示 App Store server notifications V2
版本。
四、购买流程变化
如果有服务端,权益发放在服务端,则流程如下:
客户端流程
- 客户端通
StoreKit SDK
拉起支付 - 用户输入密码或
FaceID
完成支付 -
StoreKit
回调客户端并传入Transaction
- 客户端校验该
Transaction
,校验通过后,将Transaction
发送给服务端
服务端流程1
- 接收来自客户端的
Transaction
并校验,校验通过后,为用户发放权益
服务端流程2
- 接收来自
Apple Server
的回调通知,根据通知的内容,对用户发放权益
服务端流程3
- 定期调用交易历史查询接口,为在回调通知漏掉的交易补发权益。
其中,服务端流程1和流程2是分开并行存在的,且流程1可选(回调通知较慢,加上流程1能够提升用户体验);流程2必须有,是唯一能够及时知悉所有交易发生的时机;流程3用于补单或者恢复购买。
注意事项:对于服务端流程3,由于交易历史查询接口需要输入originalTransactionId
,所以如果用户的首次交易被遗漏,是没有办法查询的。所以它需要客户端协助:客户端获取当前Apple ID的任意一条交易上传服务端,服务端取其中的originalTransactionId
查询交易历史,再取交易历史的最近一条交易作为发放权益的依据。
五、服务器迁移到 JWS 格式交易验证
对于 StoreKit 2
,苹果已经废弃了用 receipt
收据验证逻辑,只需要提供交易的 originalTransactionId
即可获取到完整的交易信息。那么如何从 StoreKit 1
升级到 StoreKit 2
呢?
如果开发者需要兼容 StoreKit v1
版本,那么还可以使用receipt
收据通过苹果接口 /verifyReceipt
验证收据,收据中是包含 originalTransactionId
的,所以,开发者可以通过inApps/v1/history
接口,随时了解交易的状态。
- 上传
receipt
到我们的服务器 - 拿着
receipt
去苹果服务器验证,并获取originalTransactionId
信息 - 根据
originalTransactionId
去苹果服务器获取历史交易记录,找到特定Transaction
六、注意事项
业务用户识别
Apple服务的回调通知中,并不会携带Apple ID信息,因此无法区分该通知属于哪个具体业务用户。Apple的提供的方式是appAccountToken
字段,该字段在客户端发起支付时指定,在通知中携带,以便业务后端区分。关于它注意几个点。
-
appAccountToken
由业务后端自己生成维护 - 如果用户自行在控制台操作,可能出现回调通知不带
appAccountToken
的情况。此时可以从交易历史中查询(第一条交易一定会带appAccountToken
,因为首次发起购买一定是从我们的APP客户端,就一定会设置)
JWS签名验证
StoreKit2
的一个重要变化是,大部分信息都采用JWS
进行组织。Transaction
是JWS
、通知也是JWS
。上面说的交易流程中,客户端交易成功上传Transaction
时,后端需要验证其有效性;接收到通知时,也要验证其有效性。
客户端上传交易信息的验证
对于后端的处理,官方并没有规定一定要将客户端得到的Transaction
传到服务端,加这一步只是我们为了实时性而做的。此时的签名就要我们自己来处理了。其实是有两种方式进行确认的
- 在业务系统已有的安全传输条件下直接传输交易信息,比如业务系统已有登录鉴权,可以相信经过登录后传输的信息是可信的
- 将整个原始的
Transaction JWS
传输给后端,后端自己验证,验证逻辑同下文“通知的签名验证”一致。
通知的签名验证
这一点是最多人搞不清楚的,我们观察任意一个通知的Header
如下。没有kid
字段,取而代之的是x5c
,这代表提供验证公钥的是一个证书X.509
证书链。我们需要先验证证书链的正确性,再用证书链提供的公钥验证整个JWS
的正确性。
{
"alg": "ES256",
"x5c": [
"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"
]
}
验证逻辑如下
- 从Apple官网下载根证书
- 取证书链的最后一个,和上述下载的根证书对比,如果不同则验证失败
- 验证证书链:第一个证书用第二个证书验证、第二个用第三个验证、以此类推,全都成功才算通过
- 从第一个证书取得公钥
- 用上一步得到的公钥验证整个
JWS
服务器宕机
如果因为服务端宕机或代码bug等原因,未能正确处理Apple Server
通知,Apple Server
会进行重试。重试时间分别是:在上一次尝试的基础上间隔1, 12, 24, 48, 72小时。也就是说,6天13小时后,将放弃通知重试,这期间还没能正确处理通知,将发生掉单。
此外,如果等不及通知重试,也可以主动查询交易历史进行补单。注意:交易历史查询接口需要originalTransactionId
作为路径参数,所以如果是丢了初次购买的交易信息,是无法补单的。
通知乱序
理论上通知存在乱序的可能:初次订阅,此时服务器未能正确处理交易信息,接着客户马上在设置界面升级,触发升级通知。会出现先收到升级通知,再收到初次订阅通知的情况。正确的处理方式是以升级通知的交易为准,忽略初次订阅通知。
为保证无论什么时候来通知,都能正确处理,可以在每次回调时都先查询交易历史,如果通知中的交易信息是最新的,则处理,否则忽略。