本文介绍了工作围绕API购买逻辑的缺陷,在谷歌Play的计费​​API V3消耗品(与每个人都使用的耗材与API第3版)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

与计费API版本3,谷歌已经删除的区别消耗品和非消费类产品的。两人都被合并成一个所谓的管理,并在某种程度上表现为一个混合新型:您的应用程序需要主动打电话到消费的项目的方法。如果被从未为一组的SKU的完成,这些项目基本上表现为好像它们是不可消耗

介绍了拟购买流程如下文档:

  1. 启动了 getBuyIntent 呼叫的采购流程。
  2. 在得到回应捆绑从谷歌播放指示是否购买成功完成。
  3. 如果收购成功,消费购买通过一个 consumePurchase 电话。
  4. 获取从谷歌播放的响应code表示,如果消费成功完成。
  5. 如果消费是成功的,规定在您的应用程序产品。

我看到两个问题,这种做法。其一是相当明显的,超过的API文档中一个错误,但对方是相当微妙的,我还没有想出如何最好地处理它。让我们先从明显的完整性:

问题1:在单个设备丢失采购:

该文档说,一个应用程序应该调用 getPurchases 每次启动以检查,如果用户拥有任何优秀的耗材应用内商品的时间。如果是这样,该应用应该消费这些和提供相关的项目。这包括其中所述购买流被购买完成后中断的情况下,但该项目被消耗之前(即周围步骤2)。

但是,如果在购买流程的步骤4和5之间中断?即该应用程序已成功使用购买,但被杀害(电话打进来,并没有足够的内存身边,手机没电了,死机,等等)之前,它有机会提供产品给用户。在这种情况下,购买将不再被包含在 getPurchases 和基本用户没有收到什么,他支付的(插入生气支持电子邮件和一星级审查这里) ...

幸运这个问题是相当容易通过引入期刊(像在文件系统)来改变采购流程,以更多的东西是这样的(步骤1和2同上):

  • 如果收购成功,使进入杂志说:增加硬币从300到400,一旦购买的<订单 - 在这里与GT; 的成功消费

  • 在日记帐分录确认,消费购买通过一个 consumePurchase 电话。

  • 获取从谷歌播放的响应code表示,如果消费成功完成。
  • 如果消费是成功的,规定在您的应用程序产品。
  • 在当前配置是确认,更改日志条目。购买的<订单 - 在这里> 的完成了
  • 然后,每一个应用程序启动时,它不应该只是检查 getPurchases ,也有杂志。如果对于没有报道 getPurchases 一个不完整的采购条目有,请继续步骤6。如果以后 getPurchase 应该还回的再次拥有的订单ID(例如,如果消费失败后,所有的),根本不理会事务,如果该杂志列出了此订单ID为完成。

    本的应该的修复问题1,但请不要让我知道如果你发现这个方法任何瑕疵。

    问题2:问题涉及到多个设备时:

    让我们假设一个用户拥有两个两个设备(手机和平板电脑等)具有相同的帐户。

    他(或她 - 从现在开始暗示)可以尝试购买更多的硬币在他的电话,然后该应用程序可以被打死在购买完成之后,却消耗了。现在,所有的如果他打开了他的平板电脑上的应用下一步, getPurchases 将报告该产品。

    在平板电脑上的应用程序将不得不假定购房发起那里,它死了创建日记帐分录之前,所以这将创建日记帐分录,消费产品,和提供的硬币。

    如果手机应用程序死亡面前有一个机会,使日记帐分录,硬币将永远不会在手机上供应的(插入生气支持电子邮件和一星级审查这里)的。如果在创建日记帐分录后的手机应用程序死了,硬币将在手机上配置的,基本上让用户在平板电脑上购买免费的(插入损失的收入在​​这里) 的。

    围绕这一点的一种方法是增加了一些独特的安装或设备ID作为有效载荷,以购买,检查购买是否是为了此设备。然后,该平板电脑可以简单地忽略了购买,只有手机将永远信贷硬币和消费的项目。

    但是:由于SKU仍然在这一点上,用户的财产,Play商店不会让用户买的另一个副本,所以基本上,直到用户再次启动对他的应用程序手机完成未决事务,他将不能购买任何更多的虚拟币在平板电脑上的(插入生气支持电子邮件,一星级的审查,并在这里损失的收入)的。

    有一种优雅的方式来处理这种情况?唯一的解决方案,我能想到的是:

    • 显示一条消息给用户请先启动另一台设备上的应用程序(呸!)
    • 或添加多个SKU为同一消耗品(应该工作,但仍然呸!)

    有没有更好的办法?还是我也许只是从根本上误解的东西,实在是没有问题吗? (我知道这个问题的机会永远上来很渺茫,但有一个足够大的用户群,不太可能最终成为所有的时间。)

    解决方案

    下面是解决这一切的最简单的方法,我来了这么远。这不是最优雅的方式,但至少它应该工作:

    1. 生成一个全球唯一的购入ID 并在本地存储在设备上。
    2. 启动与 getBuyIntent 购买流程与在购入ID 作为开发商的有效载荷。
    3. 在得到回应捆绑从谷歌播放指示是否购买成功完成。
    4. 如果购买成功,提供的产品和记住的购入ID 作为完成(必须自动完成的)。
    5. 如果该配置成功,消费购买通过一个 consumePurchase 调用
      (我这样做的发射后不管的方式)。

    每一个应用程序被启动时,通过以下内容:

    1. 发送 getPurchases 请求查询拥有的应用程序内产品的用户。
    2. 如果发现任何消费产品,检查的购入ID 在开发者有效载荷被存储在设备上。如果不是,忽略了产品。
    3. 如果产品有本地的购入ID ,检查 购入ID 包括在完成列表。如果没有,请继续上面的步骤4,否则,继续在上面的步骤5。

    以下是事情都可能出错在单一设备上会发生什么,然后:

    • 如果购买永远不会启动或不完整的,用户没有得到收取和应用程序返回到pre-购买状态,用户可以再试一次。未使用的购入ID 仍是在本地-list,但是这应该只是一个相当小的内存泄漏,可以固定一些到期逻辑
    • 如果购买完成,但应用程序死4步之前,当它被重新启动,发现未决购买(产品仍报以独资),并继续执行步骤4。
    • 如果应用程序第4步后死亡,但该产品在消费之前,应用程序查找在重新启动挂起的购买,但知道要忽略它作为在购入ID 是在已完成的列表。该应用程序只需继续第5步。

    在多设备情况,任何其他设备会简单地忽略任何非本地申请中的采购(耗材报道独资)作为在购入ID 是不是在该设备的本地列表。

    的一个问题是,一个悬而未决的购买将prevent其它设备能够启动一个并行购买相同的产品。所以,如果一个用户有一个未完成的事务执行步骤2和5之间的某个地方卡住(即购买完成后,但在此之前消耗完毕)在他的电话,他将无法做任何更多的购买同样的产品在他的平板电脑,直到该应用程序完成第5步,即消费产品,在手机上。

    这个问题可以很容易地通过添加多个副本每个易损SKU的解决(但不武)为谷歌播放和不断变化的第2步中的第一个列表(5也许?):

  • 在集与 getBuyIntent 购入ID启动采购流程作为下一个可用的SKU 作为开发商的有效载荷。
  • 可编程性上的说明(为了增加难度黑客的):

    1. 完成通过自由APK 或类似的:
      这些应用程序基本上是模仿谷歌Play商店完成购买。检测到它们,需要验证签名包含在购买收据,拒绝没有通过检查,其中大多数应用程序不这样做(的)。问题解决了在大多数情况下(见第4点)。
    2. 耗材通过游戏杀手或类似的增加应用内账户余额:
      这些应用程序将试图找出其中的内存(或本地存储),您的应用程序商店的硬币或其他消费产品的当前数量直接修改的次数。为了使这个困难(即不可能为普通用户),需要想出一个方法来存储帐户余额不作为纯文本的整数,但在一些加密方式或伴随着一些校验。问题解决了在大多数情况下(见第4点)。
    3. 杀应用程序在正确的时间和玩弄其本地存储:
      如果有人购买他们的电话消耗品并设法杀害应用程序该产品已设置后,但在此之前它已经被消耗(可能很难给力),他们可以再修改本地存储在他们的添加在购入ID 以本地列表,以便在每个设备上颁发一旦产品。或者,他们可能破坏已完成列表中的购买标识在手机上,并重新启动应用程序,以获得该奖项的两倍。如果他们再(通过简单地设置手机的飞行模式和删除谷歌Play商店缓存容易吧)管理供应之后,但在产品的消费前杀应用程序,他们能保持这种方式偷窃越来越多的产品。此外,混淆或执行校验存储可以使这个更难。
    4. 反编译,开发一个补丁应用程序:
      这种做法,当然,允许黑客以pretty的多少呼风唤雨与您的应用程序(包括违反任何对策采取以减轻点1和2),这将是非常难以prevent完全。但它可以更难黑客通过使用code模糊处理( ProGuard的)和过于复杂的逻辑的关键采购管理code(可能会导致马车code,虽然如此,这并不一定是最好的想法)。另外,code可以写的方式,它的逻辑可以在不影响其功能,以允许定期部署该打破任何可用补丁替换版本进行修改。

    总的来说,签名验证的采购和一些相对简单,但非明显的校验的有关数据或签名(在存储器和在本地存储器)的的是足以迫使黑客反编译(或其他反向工程)的应用程序,以窃取产品。除非应用程序变得非常流行,这应该是一个足够的威慑力。在code加之有些频繁的更新,打破任何开发补丁可以保持应用程序的移动目标的黑客灵活的逻辑。

    请记住,我可能会忘记一些其他的黑客。请评论,如果你知道一个。

    结论:

    总的来说,这不是最干净的解决方案,因为人们需要维护多个并行的SKU为每种耗材产品,但到目前为止,我还没有想出一个更好的,实际上解决的问题。

    所以,请不要共享你可能有任何其他的想法。 + 1`s保证任何好的指针。 :)

    With version 3 of the Billing API, Google has removed the distinction between consumable and non-consumable products. Both have been combined into a new type called "managed" and behave somewhat like a hybrid: Your app needs to actively call a method to "consume" the items. If that is never done for a set of skus, those items basically behave as if they were non-consumable.

    The documentation describes the intended purchase flow as follows:

    1. Launch a purchase flow with a getBuyIntent call.
    2. Get a response Bundle from Google Play indicating if the purchase completed successfully.
    3. If the purchase was successful, consume the purchase by making a consumePurchase call.
    4. Get a response code from Google Play indicating if the consumption completed successfully.
    5. If the consumption was successful, provision the product in your application.

    I see two problems with this approach. One is fairly obvious and more a "bug" in the documentation than the API, but the other is rather subtle and I still haven't figured out how to best handle it. Let's start with the obvious one for completeness:

    Problem 1: Lost purchases on single device:

    The docs say that an app should call getPurchases every time it is launched to "check if the user owns any outstanding consumable in-app products". If so, the app should consume these and provision the associated item. This covers the case where the purchase flow is interrupted after the purchase is completed, but before the item is consumed (i.e. around step 2).

    But what if the purchase flow is interrupted between step 4 and 5? I.e. the app has successfully consumed the purchase but it got killed (phone call came in and there wasn't enough memory around, battery died, crash, whatever) before it had a chance to provision the product to the user. In such a case, the purchase will no longer be included in getPurchases and basically the user never receives what he paid for (insert angry support email and one-star review here)...

    Luckily this problem is fairly easy to fix by introducing a "journal" (like in a file system) to change the purchase flow to something more like this (Steps 1 and 2 same as above):

    1. If the purchase was successful, make entry into journal saying "increase coins from 300 to 400 once purchase <order-id here> is successfully consumed."

    2. After journal entry is confirmed, consume the purchase by making a consumePurchase call.

    3. Get a response code from Google Play indicating if the consumption completed successfully.
    4. If the consumption was successful, provision the product in your application.
    5. When provisioning is confirmed, change journal entry to "purchase <order-id here> completed".

    Then, every time the app starts, it shouldn't just check getPurchases, but also the journal. If there is an entry there for an incomplete purchase that wasn't reported by getPurchases, continue at step 6. If a later getPurchase should ever return that order ID as owned again (e.g. if the consumption failed after all), simply ignore the transaction if the journal lists this order ID as complete.

    This should fix problem 1, but please do let me know if you find any flaws in this approach.

    Problem 2: Issues when multiple devices are involved:

    Let's say a user owns two devices (a phone and a tablet, for example) with the same account on both.

    He (or she - to be implied from now on) could try to purchase more coins on his phone and the app could get killed after the purchase completed, but before it is consumed. Now, if he opens the app on his tablet next, getPurchases will report the product as owned.

    The app on the tablet will have to assume that the purchase was initiated there and that it died before the journal entry was created, so it will create the journal entry, consume the product, and provision the coins.

    If the phone app died before it had a chance to make the journal entry, the coins will never be provisioned on the phone (insert angry support email and one-star review here). And if the phone app died after the journal entry was created, the coins will also be provisioned on the phone, basically giving the user a purchase for free on the tablet (insert lost revenue here).

    One way around this is to add some unique install or device ID as a payload to the purchase to check whether the purchase was meant for this device. Then, the tablet can simply ignore the purchase and only the phone will ever credit the coins and consume the item.

    BUT: Since the sku is still in the user's possession at this point, the Play Store will not allow the user to buy another copy, so basically, until the user launches the app again on his phone to complete the pending transaction, he will not be able to purchase any more virtual coins on the tablet (insert angry support email, one-star review, and lost revenue here).

    Is there an elegant way to handle this scenario? The only solutions I can think of are:

    • Show a message to the user to please launch the app on the other device first (yuck!)
    • or add multiple skus for the same consumable item (should work, but still yuck!)

    Is there a better way? Or am I maybe just fundamentally misunderstanding something and there really is no issue here? (I realize that the chances of this problem ever coming up are slim, but with a large enough user-base, "unlikely" eventually becomes "all-the-time".)

    解决方案

    Here's the simplest way to fix all this, that I have come up with so far. It's not the most elegant approach, but at least it should work:

    1. Generate a globally unique purchase ID and store it locally on the device.
    2. Launch a purchase flow with getBuyIntent with the purchase ID as the developer payload.
    3. Get a response Bundle from Google Play indicating if the purchase completed successfully.
    4. If purchase was successful, provision the product and remember the purchase ID as completed (this must be done atomically).
    5. If the provisioning was successful, consume the purchase by making a consumePurchase call
      (I do this in a "fire-and-forget" manner).

    Every time the app is launched, go through the following:

    1. Send a getPurchases request to query the owned in-app products for the user.
    2. If any consumable products are found, check if the purchase ID in the developer payload is stored on the device. If not, ignore the product.
    3. For products with a "local" purchase ID, check if the purchase ID is included in the completed-list. If not, continue at step 4 above, otherwise continue at step 5 above.

    Here's how things can go wrong on a single device and what happens then:

    • If the purchase never starts or doesn't complete, the user doesn't get charged and the app goes back to the pre-purchase-state and the user can try again. The unused purchase ID still is in the "local"-list, but that should only be a fairly minor "memory-leak" that can be fixed with some expiration-logic.
    • If the purchase completes, but the app dies before step 4, when it gets restarted, it finds the pending purchase (the product is still reported as owned) and can continue with step 4.
    • If the app dies after step 4 but before the product is consumed, the app finds the pending purchase on restart, but knows to ignore it as the purchase ID is in the completed-list. The app simply continues with step 5.

    In the multiple-device-case, any other device will simply ignore any non-local pending purchases (consumables reported as owned) as the purchase ID is not in that device's local list.

    The one issue is that a pending purchase will prevent other devices from being able to start a parallel purchase for the same product. So, if a user has an incomplete transaction stuck somewhere between step 2 and 5 (i.e. after purchase completion, but before consumption completion) on his phone, he won't be able to do any more purchases of the same product on his tablet until the app completes step 5, i.e. consumes the product, on the phone.

    This issue can be resolved very easily (but not elegantly) by adding multiple copies (5 maybe?) of each consumable SKU to Google Play and changing step 2 in the first list to:

    1. Launch a purchase flow for the next available SKU in the set with getBuyIntent with the purchase ID as the developer payload.

    A note on hackability (in order of increasing difficulty for the hacker):

    1. Completing fake purchases via Freedom APK or similar:
      These apps basically impersonate the Google Play Store to complete the purchase. To detect them, one needs to verify the signature included in the purchase receipt and reject purchases that fail the check, which most apps don't do (right). Problem solved in most cases (see point 4).
    2. Increasing in-app account balance of consumable via Game Killer or similar:
      These apps will try to figure out where in memory (or local storage) your app stores the current number of coins or other consumable products to modify the number directly. To make this harder (i.e. impossible for the average user), one needs to come up with a way to store the account balance not as a "plain-text" integer, but in some encrypted way or along with some checksums. Problem solved in most cases (see point 4).
    3. Killing the app at the right time and messing with its local storage:
      If someone purchases a consumable product on their phone and manages to kill the app after the product has been provisioned but before it has been consumed (likely very difficult to force), they could then modify the local storage on their tablet to add the purchase ID to the local list to have the product awarded once on each device. Or, they could corrupt the list of completed purchase IDs on the phone and restart the app to get the award twice. If they again manage to kill the app after provisioning but before consumption of the product (easy now by simply setting the phone to airplane mode and deleting the Google Play Store Cache), they can keep stealing more and more product in this way. Again, obfuscating or checksumming the storage can make this much harder.
    4. Decompiling and developing a patch for the app:
      This approach, of course, allows the hacker to pretty much do anything they want with your app (including breaking any countermeasures taken to alleviate points 1 and 2) and it will be extremely hard to prevent entirely. But it can be made harder for the hacker by using code obfuscation (ProGuard) and overly complex logic for the critical purchase-management code (might lead to buggy code, though, so this is not necessarily the best idea). Also, the code can be written in a way that its logic can be modified without affecting its function to allow for regular deployment of alternate versions that break any available patches.

    Overall, signature verification for the purchases and some relatively simple but non-obvious checksumming or signing of the relevant data (in memory and in the local storage) should be sufficient to force a hacker to decompile (or otherwise reverse-engineer) the app in order to steal product. Unless the app gets hugely popular this should be a sufficient deterrent. Flexible logic in the code combined with somewhat frequent updates that break any developed patches can keep the app a moving target for hackers.

    Keep in mind that I might be forgetting some other hacks. Please comment if you know of one.

    Conclusion:

    Overall, this is not the cleanest solution as one needs to maintain multiple parallel SKUs for each consumable product, but so far I haven't come up with a better one that actually fixes the issues.

    So, please do share any other ideas you might have. +1`s guaranteed for any good pointers. :)

    这篇关于工作围绕API购买逻辑的缺陷,在谷歌Play的计费​​API V3消耗品(与每个人都使用的耗材与API第3版)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

    10-30 10:39