IOS库:IAP StoreKit2 服务器

苹果服务器与开发者服务器之间的通讯,包括苹果通知、开发者主动请求苹果服务器、新的验证收据流程等。

  • 以前只支持 Apple server 单向发送状态变化的通知给我们自己的服务器,现在支持主动去请求 Apple server 获取历史交易记录
  • StoreKit 2 服务器端接口支持了新的格式(JWS 格式)。
  • 沙盒环境测试优化

构建开发者服务器可以实现以下几个功能:

  • 接收内购状态改变通知
  • 通过接口跟踪内购状态变化(获取所有的交易历史记录)
  • 随时验证用户的访问权限(是否已购买,是否已退款等信息)
  • 跟踪退款信息

一、验证票据

开发者可以用 receipt 收据或者使用StoreKit v2 新的 signed transactiond 来验证订单。

Receipt 收据验证方式:

  • 在用户设备App中验证收据
  • 在开发者服务端通过苹果/verifyReceipt接口验证收据

服务器端在通过/verifyReceipt接口验证票据时,新 API 的数据结构也发生了变化。例如统一了购买时间、过期时间、原始购买时间格式,新增了 appAcountToken 字段、内购类型字段、退款时间、退款原因、促销优惠类型等。

旧的 receipts收据内容如下图:

新的 JWS 格式的交易格式内容,如下图。对比 receipts 收据,可以知道有那些变化。旧的有 GMT(格林威治标准时间)、PST(太平洋标准时间)、Unix timestampUnix 时间戳),新的格式,只保留了 Unix 时间戳,并且字段做了更新。

内购的类型,也有返回了。

这个就是上面提到的关系的用户信息的 UUID。这里苹果用 appAccountToken 字段。

这个是用户退款时间和退款原因的字段。从之前的 cancellation_date 改成现在的 revocationData

最后是促销优惠的类型。


二、新增了一些 API

内购历史订单查询 API

获取交易历史记录:get_transaction_history,只需要用户任意一个交易里的originalTransactionId即可获取所有的历史记录。需要注意的是,这个返回的数据有一个字段 hasMoreture,表示历史订单有更新。


查找收据发票 API

这个就是前言提到的用户收到苹果的收据发票时,无法与开发者的订单匹配的问题!现有有新的 API 来解决了,这个新的接口,可以让用户提供的发票上的ORDER ID查到对应的transaction交易信息。整个流程,主是这样,用户客诉,提供订单 ID,查询到状态,为用户进行补单或者支持服务。开发者服务器记得保存对应的用户订单 ID 做好映射。


查找该客户过去的退款 API

除了苹果服务器的通知退款REFUND后,开发者现在可以主动通过inApps/v1/history接口,查询用户的所有交易订单,来确认订单的状态是不是退款(撤销)。

苹果提供了查询所有内购订单的接口,但是不可能让开发者查一次,然后再判断那些是退款订单吧!所以,苹果提供了另一个接口。这个接口也是一样,通过用户的任一个 originalTransactionId 可以查到这个用户的所有退款记录订单。用户退款有一个单独的 refundDate 字段,如果有内容时间就表示是退款。


三、通过通知跟踪状态

回调通知通过notificationTypesubType两个字段区分,自动续期订阅相关通知如下。

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进行组织。TransactionJWS、通知也是JWS。上面说的交易流程中,客户端交易成功上传Transaction时,后端需要验证其有效性;接收到通知时,也要验证其有效性。

客户端上传交易信息的验证

对于后端的处理,官方并没有规定一定要将客户端得到的Transaction传到服务端,加这一步只是我们为了实时性而做的。此时的签名就要我们自己来处理了。其实是有两种方式进行确认的

  • 在业务系统已有的安全传输条件下直接传输交易信息,比如业务系统已有登录鉴权,可以相信经过登录后传输的信息是可信的
  • 将整个原始的Transaction JWS传输给后端,后端自己验证,验证逻辑同下文“通知的签名验证”一致。
通知的签名验证

这一点是最多人搞不清楚的,我们观察任意一个通知的Header如下。没有kid字段,取而代之的是x5c,这代表提供验证公钥的是一个证书X.509证书链。我们需要先验证证书链的正确性,再用证书链提供的公钥验证整个JWS的正确性。

{
  "alg": "ES256",
  "x5c": [
    "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"
  ]
}

验证逻辑如下

  1. Apple官网下载根证书
  2. 取证书链的最后一个,和上述下载的根证书对比,如果不同则验证失败
  3. 验证证书链:第一个证书用第二个证书验证、第二个用第三个验证、以此类推,全都成功才算通过
  4. 从第一个证书取得公钥
  5. 用上一步得到的公钥验证整个JWS
服务器宕机

如果因为服务端宕机或代码bug等原因,未能正确处理Apple Server通知,Apple Server会进行重试。重试时间分别是:在上一次尝试的基础上间隔1, 12, 24, 48, 72小时。也就是说,6天13小时后,将放弃通知重试,这期间还没能正确处理通知,将发生掉单。

此外,如果等不及通知重试,也可以主动查询交易历史进行补单。注意:交易历史查询接口需要originalTransactionId作为路径参数,所以如果是丢了初次购买的交易信息,是无法补单的。

通知乱序

理论上通知存在乱序的可能:初次订阅,此时服务器未能正确处理交易信息,接着客户马上在设置界面升级,触发升级通知。会出现先收到升级通知,再收到初次订阅通知的情况。正确的处理方式是以升级通知的交易为准,忽略初次订阅通知。

为保证无论什么时候来通知,都能正确处理,可以在每次回调时都先查询交易历史,如果通知中的交易信息是最新的,则处理,否则忽略。

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容