问题描述
最近公司发现公司发现有人通过苹果内购充值,实际上苹果后台查询充值记录并没有相关记录,初步判断可能内购流程出现了问题进行排查。
苹果内购流程图
通用流程梳理
- IOS SDK 请求服务器,创建订单;
- 服务器生成订单,并返回订单号;
- 发起支付,如果没有登陆会要求用户登陆appleID,如果已经登陆,会弹出购买确认;
- 点击购买,等待苹果返回支付结果;
- 如果支付成功,苹果会返回receipt-data 数据,与订单号一起发到服务器进行校验;
- 接收到IOS SDK参数进行校验,成功后请求APPLE 服务校验支付结果;
- 验证返回结果返回进行业务操作,并返回IOS SDK最终支付结果;
问题原因
苹果服务器再返回给我们服务器订单结果。receipt_data 在越狱环境下是可以被插件伪造的,后台向苹果验证时,居然还能验证通过。下面是几种receipt_data 请求APPLE 服务器返回的结果
正常返回JSON格式为:
第一种
{
"status": 0,
"environment": "Sandbox",
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "com.xxx.xxx",
"application_version": "84",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2016-12-05 08:41:57 Etc/GMT",
"receipt_creation_date_ms": "1480927317000",
"receipt_creation_date_pst": "2016-12-05 00:41:57 America/Los_Angeles",
"request_date": "2016-12-05 08:41:59 Etc/GMT",
"request_date_ms": "1480927319441",
"request_date_pst": "2016-12-05 00:41:59 America/Los_Angeles",
"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
"original_purchase_date_ms": "1375340400000",
"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
"original_application_version": "1.0",
"in_app": [
{
"quantity": "1",
"product_id": "*******【支付定义产品ID】*******",
"transaction_id": "10000003970",
"original_transaction_id": "10000003970",
"purchase_date": "2016-12-05 08:41:57 Etc/GMT",
"purchase_date_ms": "1480927317000",
"purchase_date_pst": "2016-12-05 00:41:57 America/Los_Angeles",
"original_purchase_date": "2016-12-05 08:41:57 Etc/GMT",
"original_purchase_date_ms": "1480927317000",
"original_purchase_date_pst": "2016-12-05 00:41:57 America/Los_Angeles",
"is_trial_period": "false"
}
]
}
}
这里边in_app 可能会出现多条数据情况,每当有一笔交易发起的时候,in_app里就会添加收据的一些信息。这些信息会一直保存直到你结束这笔交易。在此之后,下次更新收据时会将其从收据中删除 - 例如,当用户再次购买时,或者您的应用明确刷新收据时。
非消耗型项目,自动续期订阅,非续期订阅或免费订阅的应用内购买收据将无限期保留在收据中。
第二种
{
"receipt": {
"original_purchase_date_pst": "2016-12-03 01:11:01 America/Los_Angeles",
"purchase_date_ms": "1480756261254",
"unique_identifier": "96f51b28f628493709966f33a1fe7ba",
"original_transaction_id": "1000000255766",
"bvrs": "82",
"transaction_id": "1000000255766",
"quantity": "1",
"unique_vendor_identifier": "FE358-1362-40FD-870F-DF788AC5",
"item_id": "11822945",
"product_id": ""*******【支付定义产品ID】*******"",
"purchase_date": "2016-12-03 09:11:01 Etc/GMT",
"original_purchase_date": "2016-12-03 09:11:01 Etc/GMT",
"purchase_date_pst": "2016-12-03 01:11:01 America/Los_Angeles",
"bid": "com.xxx.xxx",
"original_purchase_date_ms": "1480756261254"
},
"status": 0
}
查看资料说是在IOS7以前版本支付和现在版本支付验证返回不用的数据结构。
越狱订单receipt_data向苹果服务器校验后如下:
{
"status": 0,
"environment": "Production",
"receipt": {
"receipt_type": "Production",
"adam_id": 1377028992,
"app_item_id": 1377028992,
"bundle_id": "com.xxx.xxx",
"application_version": "3",
"download_id": 80042231041057,
"version_external_identifier": 827853261,
"receipt_creation_date": "2018-07-23 07:30:45 Etc/GMT",
"receipt_creation_date_ms": "1532331045000",
"receipt_creation_date_pst": "2018-07-23 00:30:45 America/Los_Angeles",
"request_date": "2018-07-23 07:33:54 Etc/GMT",
"request_date_ms": "1532331234485",
"request_date_pst": "2018-07-23 00:33:54 America/Los_Angeles",
"original_purchase_date": "2018-07-01 12:16:21 Etc/GMT",
"original_purchase_date_ms": "1530447381000",
"original_purchase_date_pst": "2018-07-01 05:16:21 America/Los_Angeles",
"original_application_version": "3",
"in_app": [ ]
}
}
解决方法
将校验逻辑进行修改,不能只校验status=0
。
- 首先客户端传参数需要增加:product_id,transaction_id
//该方法为监听内购交易结果的回调
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
transactions 为一个数组 遍历就可以得到 SKPaymentTransaction 对象的元素transaction。然后从transaction里可以取到以下这两个个参数,product_id,transaction_id。另外从沙盒里取到票据信息receipt_data
我们先看怎么取到以上的三个参数
//获取receipt_data
NSData *data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
NSString * receipt_data = [data base64EncodedStringWithOptions:0];
//获取product_id
NSString *product_id = transaction.payment.productIdentifier;
//获取transaction_id
NSString * transaction_id = transaction.transactionIdentifier;
这是我们必须要传给服务器的三个字段。以上三个字段需要做好空值校验,避免崩溃。
下面我们来解释一下,为什么要给服务器传这三个参数。
receipt_data:这个不解释了 大家都懂 不传的话 服务器根本没法校验
product_id:这个也不用解释 内购产品编号 你不传的话 服务器不知道你买的哪个订单
transaction_id:这个是交易编号,是必须要传的。因为你要是防止越狱下内购被破解就必须要校验in_app这个参数。而这个参数的数组元素有可能为多个,你必须得找到一个唯一标示,才可以区分订单到底是那一笔。
- 服务器逻辑修改
- 先判断是否重复分发内购产品。收到客户端上报的transaction_id 后,直接MD5后去数据库查,能查到说明是重复订单,返回相应错误码给客户端,查不到去苹果那边校验。
沙箱校验地址 = “https://sandbox.itunes.apple.com/verifyReceipt”;
正式校验地址 = “https://buy.itunes.apple.com/verifyReceipt”; - 服务器拿到苹果校验结果,首先判断订单状态是否成功。
- 如果订单成功在判断格式为IOS7 之前还是之后。
- 如果为IOS7 之前版本直接判断transaction_id、product_id;
- 如果为IOS7 之后判断in_app 是否有值,如果没有直接返回失败,如果存在遍历查询对应transaction_id并比较product_id;
- 以上校验都正确就可以把订单充值进去,给用户分发内购产品。
- 先判断是否重复分发内购产品。收到客户端上报的transaction_id 后,直接MD5后去数据库查,能查到说明是重复订单,返回相应错误码给客户端,查不到去苹果那边校验。
注:
后台传入参数一定要进行存储。
解析
说明:in_app参数可能为空,如果为空得话,也需要将这笔交易认为是有效的交易。当然我们肯定不能这么干,这个参数是必须必须要校验的,不然越狱环境下,分分钟就把你内购破解了。我去校验了很多正常用户的内购订单,没发现一个in_app参数是为空的。但为了保险,还是让后台把所有前端传的receipt_data等参数不管成功失败都保存下来,万一哪个用户因此投诉充值不到账,我们有据可查。
参考:https://www.jianshu.com/p/5cf686e92924
参考:https://www.jianshu.com/p/7e7c3a918946utm_campaign=hugo&utm_medium=reader_share&utm_content=note&utm_source=qq