iOS内购-防越狱破解刷单

2023-11-09

---------------------------2018.10.16更新---------------------------

最近我们公司丢单率上涨,尤其是10月份比9月份来说丢单率翻了3倍,和一些同行交流了一下,发现他们也是丢单量增加,初步推断可能是苹果iOS12的原因,某些情况下会有用户内购成功后,却返回的是订单失败,错误类型为SKErrorUnknown。目前客户端好像没办法去解决。如果有小伙伴和我一样也遇到过相同的问题话,请私信我下,我们都多互相交流一下。

---------------------------2018.10.16更新---------------------------

---------------------------以下为正文---------------------------

iOS内购开发大家一定不陌生,网上类似的文章能搜出千八百篇。大部分都是围绕着如何实现?如何防止漏单丢单说明的。很少有提及到越狱的,即使偶尔有一两篇说越狱,也是简单的三言两语说 为了安全,我们直接屏蔽了越狱手机的内购功能。巴拉巴拉... 以前我也是这么想的,直到上个周末发现我们的内购被破解了...才有了这篇文章。本篇文章就是来讲述越狱下的内购如何防止被破解。

首先我们先简单理一下整个内购的核心流程:

  1. 客户端发起支付订单

  2. 客户端监听购买结果

  3. 苹果回调订单购买成功时,客户端把苹果给的receipt_data和一些订单信息上报给服务器

  4. 后台服务器拿receipt_data向苹果服务器校验

  5. 苹果服务器向返回status结果,含义如下,其中为0时表示成功。

  • 21000 App Store无法读取你提供的JSON数据

  • 21002 收据数据不符合格式

  • 21003 收据无法被验证

  • 21004 你提供的共享密钥和账户的共享密钥不一致

  • 21005 收据服务器当前不可用

  • 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中

  • 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证

  • 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证

6.服务器发现订单校验成功后,会把这笔订单存起来,receipt_data用MD5值映射下,保存到数据库,防止同一笔订单,多次发放内购商品。

以上应该是主流的校验流程。当然客户端其中会插一些丢单漏单的逻辑校验,因为那些跟本篇文章无关,所以不在此展开。

从上面的流程可以看出,整个内购的核心其实就是receipt_data。苹果回调给客户端,客户端上报给服务器,服务器拿到后去向苹果服务器校验,苹果服务器再返回给我们服务器订单结果。其实严格来说,整个流程是没问题的。整个的漏洞是在最后一步上,【苹果服务器再返回给我们服务器订单结果】。receipt_data在越狱环境下是可以被插件伪造的,后台向苹果验证时,居然还能验证通过。是的,你没看错,苹果这里有个贼鸡儿坑的地方。这是最坑最坑的地方,伪造的receipt_data苹果校验也返回支付成功

如何解决?我们先来看下越狱订单和正常订单对比

越狱订单receipt_data向苹果服务器校验后如下:

{
    "status": 0, 
    "environment": "Production", 
    "receipt": {
        "receipt_type": "Production", 
        "adam_id": 1377028992, 
        "app_item_id": 1377028992, 
        "bundle_id": "*******【敏感信息不给看】*******", 
        "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": [ ]
    }
}
复制代码

正常订单receipt_data向苹果服务器校验后如下:

{
   {
    "status": 0, 
    "environment": "Production", 
    "receipt": {
        "receipt_type": "Production", 
        "adam_id": 1377028992, 
        "app_item_id": 1377028992, 
        "bundle_id": "*******【敏感信息不给看】*******", 
        "application_version": "3", 
        "download_id": 36042096097927, 
        "version_external_identifier": 827703432, 
        "receipt_creation_date": "2018-07-10 13:54:27 Etc/GMT", 
        "receipt_creation_date_ms": "1531230867000", 
        "receipt_creation_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
        "request_date": "2018-07-23 08:03:27 Etc/GMT", 
        "request_date_ms": "1532333007664", 
        "request_date_pst": "2018-07-23 01:03:27 America/Los_Angeles", 
        "original_purchase_date": "2018-06-13 06:52:13 Etc/GMT", 
        "original_purchase_date_ms": "1528872733000", 
        "original_purchase_date_pst": "2018-06-12 23:52:13 America/Los_Angeles", 
        "original_application_version": "5", 
        "in_app": [
            {
                "quantity": "1", 
                "product_id": "*******【敏感信息不给看】*******", 
                "transaction_id": "160000477610856", 
                "original_transaction_id": "160000477610856", 
                "purchase_date": "2018-07-10 13:54:27 Etc/GMT", 
                "purchase_date_ms": "1531230867000", 
                "purchase_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
                "original_purchase_date": "2018-07-10 13:54:27 Etc/GMT", 
                "original_purchase_date_ms": "1531230867000", 
                "original_purchase_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
                "is_trial_period": "false"
            }
        ]
    }
}
复制代码

看完两笔订单的对比我相信大家可以清楚的知道,越狱订单虽然状态返回是成功的,但是in_app这个参数是空的。大概查了一下。iOS7以下是没有这个in_app参数的,iOS7以上是有的。因为现在App基本支持的起步都是iOS8 iOS9了,iOS7可以不用管了。但这里还有一个问题,就是in_app这个字段并不总是只返回一个,有可能会返回多个,比如下面的这种订单。

正常订单receipt_data校验后  in_app多个元素时:

{
    "status":0,
    "environment":"Sandbox",
    "receipt":{
        "receipt_type":"ProductionSandbox",
        "adam_id":0,
        "app_item_id":0,
        "bundle_id":"*******【敏感信息不给看】*******",
        "application_version":"1",
        "download_id":0,
        "version_external_identifier":0,
        "receipt_creation_date":"2018-07-24 04:28:24 Etc/GMT",
        "receipt_creation_date_ms":"1532406504000",
        "receipt_creation_date_pst":"2018-07-23 21:28:24 America/Los_Angeles",
        "request_date":"2018-07-24 04:30:06 Etc/GMT",
        "request_date_ms":"1532406606695",
        "request_date_pst":"2018-07-23 21:30:06 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":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000398911598",
                "original_transaction_id":"1000000398911598",
                "purchase_date":"2018-05-16 03:26:12 Etc/GMT",
                "purchase_date_ms":"1526441172000",
                "purchase_date_pst":"2018-05-15 20:26:12 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 03:26:12 Etc/GMT",
                "original_purchase_date_ms":"1526441172000",
                "original_purchase_date_pst":"2018-05-15 20:26:12 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000398911640",
                "original_transaction_id":"1000000398911640",
                "purchase_date":"2018-05-16 03:26:37 Etc/GMT",
                "purchase_date_ms":"1526441197000",
                "purchase_date_pst":"2018-05-15 20:26:37 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 03:26:37 Etc/GMT",
                "original_purchase_date_ms":"1526441197000",
                "original_purchase_date_pst":"2018-05-15 20:26:37 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000398911784",
                "original_transaction_id":"1000000398911784",
                "purchase_date":"2018-05-16 03:26:50 Etc/GMT",
                "purchase_date_ms":"1526441210000",
                "purchase_date_pst":"2018-05-15 20:26:50 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 03:26:50 Etc/GMT",
                "original_purchase_date_ms":"1526441210000",
                "original_purchase_date_pst":"2018-05-15 20:26:50 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000398911801",
                "original_transaction_id":"1000000398911801",
                "purchase_date":"2018-05-16 03:27:22 Etc/GMT",
                "purchase_date_ms":"1526441242000",
                "purchase_date_pst":"2018-05-15 20:27:22 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 03:27:22 Etc/GMT",
                "original_purchase_date_ms":"1526441242000",
                "original_purchase_date_pst":"2018-05-15 20:27:22 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000399060767",
                "original_transaction_id":"1000000399060767",
                "purchase_date":"2018-05-16 11:10:45 Etc/GMT",
                "purchase_date_ms":"1526469045000",
                "purchase_date_pst":"2018-05-16 04:10:45 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 11:10:45 Etc/GMT",
                "original_purchase_date_ms":"1526469045000",
                "original_purchase_date_pst":"2018-05-16 04:10:45 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000399061778",
                "original_transaction_id":"1000000399061778",
                "purchase_date":"2018-05-16 11:14:52 Etc/GMT",
                "purchase_date_ms":"1526469292000",
                "purchase_date_pst":"2018-05-16 04:14:52 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 11:14:52 Etc/GMT",
                "original_purchase_date_ms":"1526469292000",
                "original_purchase_date_pst":"2018-05-16 04:14:52 America/Los_Angeles",
                "is_trial_period":"false"
            },
            ...
        ]
    }
}
复制代码

综上,整个服务器那边校验逻辑应该是这样的。

首先客户端必须要给服务器传的三个参数:receipt_data, 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这个参数。而这个参数的数组元素有可能为多个,你必须得找到一个唯一标示,才可以区分订单到底是那一笔。

所以服务器那边逻辑就很清晰了。

  1. 首先判断订单状态是不是成功。

  2. 如果订单状态成功在判断in_app这个字段有没有,没有直接就返回失败了。如果存在的话,遍历整个数组,通过客户端给的transaction_id 来比较,取到相同的订单时,对比一下bundle_id ,product_id 是不是正确的。

  3. 如果以上校验都正确再去查询一下数据库里这笔订单是不是存在,如果存在也是返回失败,避免重复分发内购商品。如果都成功了,再把这笔订单充值进去,给用户分发内购商品。

注意:一定要告诉后台,不论校验是否成功,只要客户端给服务器传了receipt_data等参数就一定要保存到数据库里。【下面会解释为什么】

以上的校验步骤,可以有效的防止内购破解,下面内容是我看苹果官方能文档的关于in_app这个参数说明和解释下为啥服务器必须要保存每一个不同的receipt_data。

苹果IAP官方文档

苹果文档上介绍in_app参数内容截图

In the JSON file, the value of this key is an array containing all in-app purchase receipts based on the in-app purchase transactions present in the input base-64 receipt-data. For receipts containing auto-renewable subscriptions, check the value of the latest_receipt_info key to get the status of the most recent renewal.

大概意思是说:

在这个JSON文件中,这个键的值是一个数组,该数组包含基于base-64后的所有内购收据。如果你的内购类型是自动更新订阅,那么请通过检查latest_receipt_info键的值,来确定最近更新的状态。

很有意思的是,苹果还特别标明了这么一句话:

Note: An empty array is a valid receipt.

也就是说这个in_app参数可能为空,如果为空的话,也需要把这笔交易认为是有效的交易。这是苹果建议的操作。当然我们肯定不能这么干,这个参数是必须必须要校验的,不然越狱环境下,分分钟就把你内购破解了。我去校验了很多正常用户的内购订单,没发现一个in_app参数是为空的。但为了保险,还是让后台把所有前端传的receipt_data等参数不管成功失败都保存下来,万一哪个用户因此投诉充值不到账,我们有据可查。

下面两段话

The in-app purchase receipt for a consumable product is added to the receipt when the purchase is made. It is kept in the receipt until your app finishes that transaction. After that point, it is removed from the receipt the next time the receipt is updated - for example, when the user makes another purchase or if your app explicitly refreshes the receipt.

The in-app purchase receipt for a non-consumable product, auto-renewable subscription, non-renewing subscription, or free subscription remains in the receipt indefinitely.

大概意思是说:

每当有一笔交易发起的时候,in_app里就会添加收据的一些信息。这些信息会一直保存直到你结束这笔交易。在此之后,下次更新收据时会将其从收据中删除 - 例如,当用户再次购买时,或者您的应用明确刷新收据时。

非消耗型项目,自动续期订阅,非续期订阅或免费订阅的应用内购买收据将无限期保留在收据中。

这一点也解释了说,为什么in_app这个数组有时候会有多个元素。

以下内容和本篇文章无关,下面只是简单分享下我了解到的越狱,以及我身边关于越狱的一些事情和对这件事情的反思。

关于越狱的一些事情:

现在越狱已经支持iOS11.3了,我的安卓同事前阵子用自己的iPhone X 越狱了。装了下插件【付费的 并且需要美国区App Store账号】他是我们产品的重度用户,很多小号。但偏偏产品因为某些原因不让做退出登录,切换账号的功能。所以一般用户如果想切换账号,只能卸载重装。但他就不一样了,他骚的飞起,在越狱机上装的插件,他可以改我的所有代码。我看了一眼。整个项目的类,方法,返回值,入参,在那个插件下一览无遗。他找到了我保存用户信息的方法,因为是公司自己人,各种业务流程,他都懂。篡改了一些我的方法,返回参数。比如一些判断bool的方法,应该返回NO,直接篡改成YES。还一些其他的东西。当然后台本身对安全的逻辑校验控制的比较弱,只在一些关键接口【支付 送礼等】做了控制。

抖音的国际板Tik Tok 如果你在国内切换成美区的App Store账号,并用vpn下载后,你会发现接口刷不到数据,看不到国外的小姐姐。我同事篡改了抖音的方法,就可以拉到数据了。具体方法。[PS:日本人发的抖音感觉都傻乎乎的,上面的小姐姐比国内的差远了。水平【化妆 滤镜 美颜 拍摄角度】明显不如国内的666]

类名:CTCarrier 方法:- (id) isoCountryCode 该方法是系统方法 拿来获取电话服务商的iOS国家编码。改成日本就是拿日本数据,改成香港就是香港数据

修改支付宝步数

类名:APStepInfo 方法:- (long long) numberofSteps 返回值就是步数,你随便改成几万步都可以。

修改支付宝朋友小红点

类名:MPBadgeView 方法:- (void) drawBadgeRedPoint 

另外,还有各种被玩坏的逆向微信功能。

是的,就是这么简单。在越狱环境下。我们写的App犹如一个被剥光衣服的小姑娘,只要越狱+V【和】P【谐】N+一些付费插件,谁都可以过来上几下。我的安卓同事不会写任何iOS的代码,不懂汇编,不懂砸壳,不懂啥iOS逆向,但他懂只要我付费几美元买个插件,一个个App就是一个个脱光衣服的姑娘。是的,现在破解的门槛真的很低,你花点钱站在巨人的肩膀上就可以为所欲为。

另外上面的几个例子,不知道大家发现了没。方法名,类名基本上都是见名知意,这也正是我们iOS的规范所在。严格的命名方式,让人一目了然,也让破解者一目了然。我大概看了,国内大厂App。抖音,支付宝,微信等等这些,没有一家做混淆的。方法名都挺规范的。这时候,我又想起了之前和同事开玩笑说的话,“以后再也特么不歧视命名-(void)a -(void)b -(void)c,label1 label2 label3的人了,人家自带混淆”。这里,我也很奇怪,虽然我知道其实就算你做了混淆对攻击者来说也是没卵用,但好歹之前是裸体小姑娘,现在是穿衣服的小姑娘了,总是会提高一些门槛把。细思了一下,大概是真的因为这玩意工作量大,容易引起很多潜在bug,收益又很低【攻击者无法是秒破变成天破而已,慢慢试总能试出来】,性价比极低,所以才不做的把。

另外,可能也有朋友会说,既然越狱机下这么多搞事情的,我能不能写个方法直接禁掉越狱机子啊。反正越狱用户也不会多,就不要了。答案是否定的,网上的一些代码,你去搜iOS判断是否越狱,iOS越狱检测等等。出来的文章基本都是复制粘贴,没什么价值的,里面的代码古老且旧,尤其是检测越狱时方法用bool值返回的那种,在一些防越狱检测的插件下,更是被秒破的。有价值的资料比我想象中的要更少。如果有朋友能有一些最新防越狱插件检测的方法,请不吝分享下。

下面在举几个大家做内购经常遇到的一些问题,和一些容易混淆的点。

Q1:内购和Apple Pay的区别?

A1:内购是内购,Apple Pay是Apple Pay。我不知道有多少人第一次接触时,会把这俩概念混淆掉,这里你可以简单这么理解,虚拟的物品就是用内购,实际的物品就是用Apple Pay。Apple Pay是一种支付方式,你可以类比为支付宝,微信那种。但人家只支持实际物品,如果你东西是虚拟的话,你却集成Apple Pay上架是要被拒绝的哦~当然反过来,实际物品你却集成内购上架,也是一样被拒。对于大部分的国内开发者而言,你很少会遇到需要集成Apple Pay的App的。能用支付宝/微信的场景还要求支持Apple Pay的产品毕竟是少数。

Q2:内购项目的类型区别?

A2:首先内购项目分为以下4种,消耗型项目,非消耗型项目,自动续期订阅,非续期订阅。我们来一个个介绍。

消耗型项目:只可使用一次的产品,使用之后即失效,必须再次购买。就是大家最广为所知的虚拟币,比如直播平台斗鱼的鱼翅,熊猫的竹子,哔哩哔哩的B币等,这个概念大家应该很好理解,不过多解释了。

非消耗型项目:只需购买一次,不会过期或随着使用而减少的产品。这个一般是游戏那里用的多,一般是付费解锁关卡的场景,用户买过一次,卸载重装或者同一个Apple id但换App账号时,也要能保证用户重新获得该内购商品。所以App内部需要额外去实现恢复购买的逻辑。

自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。iTunces上给的示例是:每月订阅提供流媒体服务的 App。对比我们熟悉的,网易云音乐的内购商品-连续包月黑胶VIP,就是此类型。一般来说,没啥必要不要选这一种,如果是VIP的那种场景推荐下面非续期订阅类型去做。自动续期订阅的坑非常多,比另外几种内购类型都要复制。

非续期订阅:一般来说VIP可以用这种方法来做订阅,我们公司项目的VIP购买就是这种方式。他的实现方式你可以完全照搬消耗性项目,不用做什么额外处理,也不用去管返回的订阅日期什么的东西,就是以服务器那边为准。服务器的日期开始,服务器的日期结束。既简单又保险,不需要额外的做什么处理。

Q3:VIP一定要用内购做吗?

A3:其实判断你们公司的App到底需不需要用内购,很简单,就是看跟实际物品有没有关系。如果你的VIP功能是类似饿了么这种,点外卖可以打折/多领红包 那么就不需要用内购,上架的时候说清楚就行了。如果你的VIP功能是虚拟的,比如头像更炫酷,尊贵的VIP身份标示,独特的入场动画等等虚拟相关的,比如QQ会员,就必须要用内购去做。需要说明的是,那种是VIP才能和某某用户聊天的场景,是VIP才能得到App里某某用户的服务【语音,视频】时,这一类的场景苹果一样认为是虚拟的,一样要用内购去做。

Q4:VIP内购一定是非续期/自动续期订阅吗?我可不可以用虚拟币购买VIP呢?

A4:这个问题我自己经历过。我的答案是你也可以用虚拟币购买VIP的这种方式,但如果被拒绝,你只能老老实实的按前种方式去做。如果你们的App既有虚拟币又有VIP,产品希望你VIP是直接用虚拟币去购买,这样整个流程都很方便。那么你一定要记住。千万不要在1.0版本这么做,这是血泪教训。1.0版本会抓的很严很严,同时虚拟币+VIP功能,百分百苹果会要求你VIP要用续期订阅去实现。最保险的做法呢,1.0版本不要做任何内购,迭代几个小版本后,加入虚拟币内购,在迭代几个小版本,加入VIP直接用虚拟币购买的功能,这是最最保险的做法。记住:1.0的审核力度是真的很严,能先不做内购就不要做内购,老板或许不懂,1.0版本什么都想要,但往往因为内购,会让你们的产品反复被拒。这一块如果大家感兴趣,可以看看,我的1.0版本就是加了内购,反复被拒5次。血泪教训

Q5:我们老板心疼那百分之30的手续费,我能不能不用内购啊,有没办法绕过内购?

A5:办法是有的。但是有风险。我16年做过绕开内购的方法。思路很简单,就是App里集成支付宝/微信/内购这种功能,后台做控制开关,审核时,开关打开,给审核人员看内购功能,审核通过后,开关关闭,给正常用户是用支付宝/微信功能。这个方法,我17年的时候听到很多群友说不行了,你在上架审核时,苹果会扫描你的包,检测到第三方支付sdk时,会拒绝掉。后来又有群友说可以用H5的方式实现支付功能。另外可能会有别的绕开苹果审核的实现方式,如果有哪位朋友知道,不妨留言告知。但不管是哪种方式,都是有风险的,苹果对内购一直抓的很严,如果让它知道你们在钱的方面上欺骗过他,后果还是很严重的。iOS上的用户付费率还是很不错的,付费意愿基本上可以是安卓用户的十几倍。所以如果你们的产品真的有前景,并且想长久做下去,还是奉劝不要做欺骗苹果的事情了。

Q6:网上有好多讲丢单的博客,看的是一脸懵逼,有的看懂后,在看下一篇又不懂了,感觉都好复杂。

A6:我在刚接触内购的时候也是这样,我觉得有些博客讲的真有点过了,它为了考虑一些用户的极端操作,多出来很多逻辑处理,导致博客异常的复杂,我记得有博客讲必须要把receipt_data等信息存到keychain里,因为用户有可能卸载App,如果你只存到NSUserDefaults里,那样就丢单了。 ......那么有没有这种情况呢?我觉得是肯定有的。但我们来算算几率,首先他内购成功,在向服务器调接口的时候,他手机突然没电了/断网了/程序崩溃了/网络差等的久他自己杀死进程了 巴拉巴拉。 然后他在下一次手机恢复正常的时候,果断卸载掉App,重新去App Store上下载安装,进入App后,发现内购没到账。

网上博客还爱用那种切换账号的场景举例,A内购成功了,但用户各种骚操作后,自己换到B账号,然后服务器那边把商品发到B账号上了,等等。

这些情况都是存在的,因为苹果的内购机制问题,你是不能百分百保证不丢单的,不要把丢单情况看的那么严重,逻辑写的那么复杂。你看看所有大厂的App上都会写充值遇到问题,点我联系客服 巴拉巴拉。关于丢单,我的做法是这样的,在苹果内购成功的回调里,NSUserDefaults存每一笔支付成功的订单,如果服务器校验成功,就把本地存的这笔订单删除。如果没收到服务器的响应,就一直保留。然后每次App启动就会去把本地存的丢单信息扔向服务器校验,校验成功删除,校验失败不管。这里还是看开发时间,当时我写内购功能的时候,预算时间就两天不到,所以写的飞快,就简单的用这个办法去防止丢单,目前来看,没有发现过一笔真正用户充钱但商品没到账的例子。如果大家开发时间充足,可以慢慢去弥补极端操作漏洞。

Q7:内购为什么会有这么多坑啊?看网上好多博客都在说,我自己做微信/支付宝的时候,没感觉有这么多坑啊

A7:苹果的内购坑主要有以下几点

applicationUsername该字段可能为nil 导致客户端没办法用这个参数给服务器透传订单编号,来形成一个交易订单号的绑定。

校验订单流程是必须服务器主动去询问苹果服务器,而支付宝/微信 却是他们的服务器会在用户支付成功时主动给我们服务器回调。正是这个原因,让iOS开发者饱受折磨,大部分的丢单漏单都是苹果的这个设计造成的。苹果不会主动回调给我们服务器,也就意味着我们服务器需要主动去苹果那里询问这笔订单,到底成没成功。但服务器询问的时机,又是客户端告诉服务器的。这就鸡儿坑了,一些情况下,用户在付费成功后,突然断网了/崩溃了/出现意外了等等,客户端没办法告诉服务器,这就出现了,用户钱成功了,内购商品却没到账。所以网上才会有这么多篇讲防止丢单的博客。

越狱下,插件也能破解掉苹果内购,然后校验状态status还返回成功。也就是本篇博客开头讲的那种情况。这一点真的是无力吐槽,亏你特么回调给我的receipt_data那么一大长串,有卵用?

苹果的订单机制。苹果为了保护用户隐私,你是看不到一条条流水明细的。你看到的只有

这种。

每一种内购类型的总收入,或者总销量。导致对账查询的时候加了不少麻烦。

苹果的退款机制。这个比上面一点更坑,iOS用户,内购了某商品,你可以在完全用完了后,联系苹果客服,说我误操作了巴拉巴拉或者说感觉这个商品不值那么多被开发者欺骗了巴拉巴拉,快给我退款,客服就会温柔的告诉你,不要急,她会帮你处理,1-2个工作日把,你就会发现你的钱就退回来了。没记错的话,一段时间内,一个Apple Id可以申请1-2次。但不能多,多了的话就会被苹果拒绝。而这一切,开发者这边是完全不知情的。你不知道哪个用户退款了,你知道的只是一个图,类似下面的这种。

用户消费了你的内购商品,公司却收不到钱,很多公司的内购服务都是要成本的。如果这种用户一旦多起来,坏账率会飙升,公司就会被活活的拖垮。一个好的项目也就凉凉掉。淘宝上关于iOS内购退款专门有一个超级庞大的黑色产业链。从弄账号到专门联系苹果客服再到道具销赃变现,各司其职,一环套一环,每个环节人都赚的盆满钵满。苦的都是公司,因为苹果没有任何损失,他也不会补偿你公司1毛钱,一切损失都是公司自己承担。没记错的话,15-16年,很多很多游戏公司都是因为这个被活活拖垮的。幸运的是,这种恶意退款一般都是针对游戏公司,因为游戏道具可以快速变现。像正常的App甚少碰到,因为他退款了也没毛用,没法及时变现。毕竟他们可不稀罕跟你们的女用户1v1视频聊天。

作者:羽化归来

链接:https://www.jianshu.com/p/5cf686e92924

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

iOS内购-防越狱破解刷单 的相关文章

  • 如何在 iOS 6 中强制 UIViewController 为纵向

    As the ShouldAutorotateToInterfaceOrientation在 iOS 6 中已弃用 我用它来强制特定视图仅肖像 在 iOS 6 中执行此操作的正确方法是什么 这仅适用于我的应用程序的一个区域 所有其他视图都可
  • iOS Twitter NSURLErrorDomain 代码=-1012

    我正在尝试通过在我的应用程序中注册 Twitter 来获取用户的联系方式 我发现this https github com malcommac DMTwitterOAuthgithub上的项目看起来非常好 我只遇到一个问题 如果我使用来自
  • Swift:UICollectionViewCell didSelectItemAtIndexPath 更改背景颜色

    我可以轻松更改单元格的背景颜色CellForItemAtIndexPath method func collectionView collectionView UICollectionView cellForItemAtIndexPath
  • Cocos2D中如何让物体对触摸做出反应?

    好吧 我开始更多地了解 Coco2D 但我有点沮丧 我发现的很多教程都是针对过时版本的代码 因此当我浏览并了解它们如何执行某些操作时 我无法将其翻译成我自己的程序 因为很多都发生了变化 话虽如此 我正在使用最新版本的 Coco2d 版本 0
  • 当 TestFlight/IAP 总是给出“无连接”错误时,如何接受“Apple 媒体服务条款和条件”?

    我正在通过 TestFlight 测试带有应用内购买的应用程序 最近 当我尝试测试购买应用内购买时 开始出现提示 Apple Media Services 条款和条件已更改 随后总是出现错误 无连接 互联网连接工作正常 如何解决这种情况并恢
  • 应用程序终止和设备重启后 PushKit 通知未到达

    借助 PushKit 我的 iOS 应用程序即使已关闭也能成功接收 VoIP 推送通知 失败时只有一个条件 如果我通过标准任务切换器刷出 终止 我的应用程序并重新启动我的设备 起初 我在重新启动设备后就遇到了这个问题 如这个问题所述 排除启
  • 在 iOS 上使用 MDCBottomNavigationBar 切换视图控制器

    我正在尝试创建一个使用 Material Design 库的底部导航功能的 iOS 应用程序 我可以获得带有底部导航栏的视图控制器来编译和显示 但我无法添加其他视图控制器并在单击不同选项卡时在它们之间切换 我将所有内容简化为两个文件 一个是
  • 如何停止覆盖数据

    我正在尝试在我的 iOS 应用程序中保存一些数据 我使用以下代码 NSArray paths NSSearchPathForDirectoriesInDomains NSDocumentDirectory NSUserDomainMask
  • 如何检查 iOS/iPadOS 是否启用了深色模式?

    从 iOS iPadOS 13 开始 提供深色用户界面风格 类似于 macOS Mojave 中引入的深色模式 如何检查用户是否启用了系统范围的深色模式 For iOS 13 您可以使用此属性来检查当前样式是否为深色模式 if availa
  • 获取 NSLayoutConstraints 关联视图

    我试图循环遍历视图约束 我向 view1 添加了 顶部 尾部 前导和高度约束 top trailing 和leading 是主ViewControllers 视图 如果我循环查看 view1 的约束 我只会看到高度约束 for constr
  • 使用 Unity3D 按钮执行 xcode 函数?

    是否可以在 unity 中制作一个按钮来执行 Xcode 中的功能 我正在尝试执行来自 unity3d 项目的推送消息 请帮忙 因为这让我发疯 提前致谢 是的 您可以调用具有 C 接口的本机 Objective C 代码 您甚至可以在 Un
  • iOS Javascript DOM“冻结?”

    这里有几个问题 有没有办法阻止 iOS 在滚动时冻结页面上的 javascript 当您在另一个选项卡中或切换应用程序时 iOS 是否会冻结 JavaScript iOS 上还有其他主要的 javascript 限制吗 iOS 6 x 会暂
  • iOS:自动调整大小不适用于 UIImageView

    我正在制作一个非常简单的应用程序来学习 Objective C 和 Xcode 该应用程序有一个 UIButton 和一个 UIImageView 当用户点击按钮时 图像从右到左以对角线运动向下移动 当它到达屏幕中的某个点时 它会重新生成以
  • 如何在没有 Apple 开发者帐户的设备上运行应用程序

    我找到了几个网站 其中提供了有关如何完成此操作的信息 但似乎没有一个网站适用于 Xcode 10 1 或 iOS 12 1 我尝试过的那些似乎都不起作用 我试过这个 创建一个空的 swift 项目 单视图应用程序 将签名团队设置为我的个人团
  • 有什么方法可以限制核心数据中的重复条目吗?

    我一直在尝试在核心数据中添加对象 所以 我希望它不应该允许核心数据存储中出现重复的条目 怎么做 这是我与保存数据相关的代码 IBAction save id sender if name text isEqualToString addre
  • iPad 上的 Cordova 锁定方向失败

    我正在使用 cordova 3 5 0 0 2 6 最后一个稳定版本 我在锁定 iPad 设备的方向时遇到问题 在 iPhone 上它可以正常工作 但在 iPad 上方向未锁定 我想锁定整个应用程序而不仅仅是页面 这是我当前的 config
  • Monotouch全局异常处理

    我在野外发现了一只令人讨厌的虫子 但我无法确定它的具体情况 有没有办法拥有全局 Try Catch 块 或者有办法处理 Monotouch 中未处理的任何异常 我可以包起来吗UIApplication Main args 在 try cat
  • 如何只允许从我的 iOS 应用程序访问我的 MySQL 数据库? (使用webapp作为数据库的网关)

    我的 iOS 应用程序需要连接到 mysql 服务器 为了实现这一目标 我想创建一个 Web 应用程序 充当客户端应用程序和服务器端数据库之间的中间人 我担心的是 有人可以简单地找出我的应用程序使用的 URL 并传递他们自己的 URL 参数
  • 以模态方式呈现 UIImagePickerController 时出错

    我有一个奇怪的问题UIImagePickerController在我的 iOS 6 应用程序中以模态方式显示 这XCode给我这个错误 Warning Attempt to present
  • 在 UIAlertController 的文本字段中选择文本

    我需要在 UIAlertController 出现后立即选择文本字段的文本 但是 我在标准 UITextField 中选择文本的方式在这里不起作用 这就是我尝试过的 但我似乎无法让它发挥作用 let ac UIAlertController

随机推荐

  • [CSDN] 批量导出博客markdown文件

    需求 为了备份或者迁移博客 需要导出博客的md格式文件 除了 爬虫 自带导出功能 编辑模式 ctrl c ctrl v 之外还有一种十分方便的方法 一行命令导出法 方法 进入 404页面 https blog console api csd
  • 自旋锁与读写锁

    1 读写锁 读写锁与互斥量类似 但是读写锁允许更高的并行性 互斥量要么是锁住多个要么是未锁住状态 而且一次只有一个线程可以对其加锁 读写锁可以有三种状态 读模式写加锁状态 写模式写加锁状态 不加锁状态 一次只有一个线程可以占有写模式下的读写
  • 四家中国企业上榜、AI 开发工具崛起,CB Insights 2022 年度 AI 100 全球榜单发布!...

    整理 郑丽媛 出品 CSDN ID CSDNnews 近日 全球知名市场数据分析机构 CB Insights 最新公布了 2022 年度 AI 100 榜单 自 2017 年起 CB Insights 每年都会发布 AI 100 榜单 在全
  • yum安装mysql 8.0

    一 安装mysql 8 0 yum源 cd etc yum repos d curl https repo mysql com mysql80 community release el7 3 noarch rpm gt centos7 my
  • 黄淮学院CSDN高校俱乐部李神龙主席发来的新年礼物感恩帖

    下午刚收到俱乐部总部给我发的礼物 心里甜甜的 先展示一下 嘿嘿 表示下半学年一定要好好工作 要不就对不起天山经理 姚希姐 付菁姐还有仲伟哥给的评语了 新的一年祝天山经理升官发财 姚希付菁姐美貌如花 仲伟哥英明神武 CSDN高校俱乐部一家人亲
  • Spring Cloud之Eureka集群与安全认证

    文章目录 前言 一 Eureka集群 1 修改配置文件为application replica1 properties 2 新增配置文件application replica2 properties 3 分别使用两个配置文件启动同一eure
  • 基本数据类型与引用数据类型的区别

    一 数据类型 java中的数据类型分为两大类 基本数据类型与引用数据类型 1 基本数据类型 基本数据类型只有8种 可按照如下分类 整数类型 long int short byte 浮点类型 float double 字符类型 char 因为
  • 拳王公社:最新虚拟资源项目赚钱成交系统,1.2W字干货大揭秘!

    小白如何快速利用虚拟资源赚到钱 本文篇幅较长 要赚钱 请耐心读完 一 选择好项目的5大要素 现在是互联网时代 信息差就是利润差 网络小白 新手找副业难 没方向 找不到好项目成为了问题 小白找副业或兼职最担心的就是承担大量资金投入 承担不明确
  • A - Odd Palindrome (回文串)

    A Odd Palindromehttps vjudge csgrandeur cn problem Gym 101652N include
  • Dom详细讲解

    1 Dom的基本介绍 1 1 什么是DOM 文档对象模型 英文全称为Document Object Model 它提供了对文档的结构化的表述 并定义了一种方式可以使从程序中对该结构进行访问 从而改变文档的结构 样式和内容 D Documen
  • pytorch环境搭建,pytorch超详细最新安装教程(一步到位)

    PyTorch是深度学习的主流框架之一 新手入门相对容易 PyTorch是一个开源的Python机器学习库 其前身是2002年诞生于纽约大学 的Torch 它是美国Facebook公司使用python语言开发的一个深度学习的框架 2017年
  • [转]element中this.$message 失效问题解决方法(使用全局调用,重新定义this)(转载请删除括号里的内容)

    这两天写项目的时候发现了这个问题 问题再现 在Model框中操作数据 在使用this message进行消息提示时发现 提示框失效 本人解决方案 具体原因我没有找出来 写这个出来也是为了让大佬指点指点 保存修改数据 handleSaveMu
  • C语言-蓝桥杯-路径

    题目 小蓝学习了最短路径之后特别高兴 他定义了一个特别的图 希望找到图 中的最短路径 小蓝的图由 2021 个结点组成 依次编号 1 至 2021 对于两个不同的结点 a b 如果 a 和 b 的差的绝对值大于 21 则两个结点 之间没有边
  • javascript控制浏览器弹窗和输出内容

    alert 这是我制作的第一行代码 这行是用于控制浏览器弹出一个
  • React使用公共文件夹public

    两者区别 其实放在两个文件夹区别就在于是否会被webpack所处理 如果您将文件放入该public文件夹 webpack 将不会处理它 在你打包的时候 会将public文件夹直接复制一份到你构建出来的文件夹中 而src assets目录的文
  • Android 三大图片加载框架比较

    1 哪三大图片加载框架 1 Picasso 2 Glide 3 Fresco 2 介绍 Picasso 和Square的网络库一起能发挥最大作用 因为Picasso可以选择将网络请求的缓存部分交给了okhttp实现 Glide 模仿了Pic
  • unity ugui text随文字增多自动调节宽度或者高度组件(备忘)

    Content Size Fitter
  • Shell编程时常用的系统文件

    10 1 Linux系统目录结构 根目录 所有文件的第一级目录 home 普通用户家目录 root 超级用户家目录 usr 用户命令 应用程序等目录 var 应用数据 日志等目录 lib 库文件和内核模块目录 etc 系统和软件配置文件 b
  • 使用redux-persist解决redux数据刷新丢失问题

    在React项目实际开发中 我们常常会对一些数据进行存储缓存中 防止用户刷新浏览器 数据丢失问题 比如token 用户信息之类的 之前都是手写一遍localStorage和sessionStorage储存 接来下 我们通过一个插件redux
  • iOS内购-防越狱破解刷单

    2018 10 16更新 最近我们公司丢单率上涨 尤其是10月份比9月份来说丢单率翻了3倍 和一些同行交流了一下 发现他们也是丢单量增加 初步推断可能是苹果iOS12的原因 某些情况下会有用户内购成功后 却返回的是订单失败 错误类型为SKE