解决 Google Play 的 Billing API v3 中消耗品的 API-purchase-logic-flaws(与使用 API v3 消耗品的所有人相关)

Working around API-purchase-logic-flaws for consumables in Google Play#39;s Billing API v3 (Relevant to everyone using consumables with API v3)(解决 Google Play 的 Billing API v3 中消耗品的 API-purchase-logic-flaws(与使用 API v3 消耗品的所有人相关))

本文介绍了解决 Google Play 的 Billing API v3 中消耗品的 API-purchase-logic-flaws(与使用 API v3 消耗品的所有人相关)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在第 3 版 Billing API 中,Google 删除了消耗品和非消耗品.两者都被组合成一种称为托管"的新类型,并且表现得有点像混合:您的应用程序需要主动调用一个方法来使用"这些项目.如果从未对一组 skus 执行此操作,那么这些项目基本上表现得就好像它们是非消耗品一样.

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. 使用 getBuyIntent 调用启动购买流程.
  2. 从 Google Play 获取响应 Bundle 指示购买是否成功完成.
  3. 如果购买成功,则通过调用 consumePurchase 来消费.
  4. 从 Google Play 获取响应代码,指示消费是否成功完成.
  5. 如果消费成功,请在您的应用程序中配置产品.
  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.

我发现这种方法存在两个问题.一个是相当明显的,并且在文档中比 API 更错误",但另一个相当微妙,我还没有弄清楚如何最好地处理它.让我们从明显的完整性开始:

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:

问题 1:单个设备上的购买丢失:

文档说应用程序应该在每次启动时调用 getPurchases 以检查用户是否拥有任何出色的应用内消耗品".如果是这样,应用程序应该使用这些并提供相关的项目.这包括在购买完成后但在商品被消费之前(即在第 2 步左右)中断购买流程的情况.

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).

但是如果购买流程在第 4 步和第 5 步之间中断怎么办?IE.该应用程序已成功消费了购买,但它在有机会向用户提供产品之前就被杀死了(电话打进来并且周围没有足够的内存,电池没电,崩溃等等).在这种情况下,购买将不再包含在 getPurchases 中,并且基本上用户永远不会收到他支付的费用(在此处插入愤怒的支持电子邮件和一星评论)...

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. 如果购买成功,请在日志中输入一旦购买<order-id here>成功消费后,将硬币从300增加到400."

  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."

确认日记帐分录后,通过调用 consumePurchase 消耗购买.

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

然后,每次应用启动时,它不应该只检查 getPurchases,还应该检查日志.如果那里有一个未由 getPurchases 报告的不完整购买条目,请继续第 6 步.如果以后的 getPurchase 应该再次将该订单 ID 返回为已拥有(例如,如果消费最终失败),如果日志将此订单 ID 列为完成,则只需忽略该交易.

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.

应该解决问题 1,但如果您发现这种方法有任何缺陷,请告诉我.

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

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

假设用户拥有两台设备(例如手机和平板电脑),并且在这两个设备上都使用相同的帐户.

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

他(或她——从现在开始暗示)可以尝试在他的手机上购买更多的硬币,并且应用程序可能会在购买完成后但在消费之前被终止.现在,如果他接下来在他的平板电脑上打开应用程序,getPurchases 将报告该产品为已拥有.

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).

解决此问题的一种方法是将一些唯一的安装或设备 ID 作为有效负载添加到购买中,以检查购买是否针对此设备.然后,平板电脑可以简单地忽略购买,只有手机会记入硬币并消费该物品.

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.

但是:由于此时 sku 仍在用户手中,因此 Play 商店将不允许用户再购买一份副本,所以基本上,直到用户在他的电脑上再次启动该应用程序手机完成待处理的交易,他将无法在平板电脑上购买更多虚拟硬币(在此处插入愤怒的支持电子邮件、一星评论和收入损失).

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:

  • 向用户显示一条消息,请先在另一台设备上启动该应用(糟糕!)
  • 或为同一个消耗品添加多个 sku(应该可以,但还是很糟糕!)

有没有更好的方法?还是我可能只是从根本上误解了某些东西而这里真的没有问题?(我意识到这个问题出现的可能性很小,但是有了足够大的用户群,不太可能"最终会变成所有时间".)

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. 生成一个全球唯一的购买 ID,并将其存储在设备本地.
  2. 使用 getBuyIntent 启动购买流程,并将 购买 ID 作为开发者有效负载.
  3. 从 Google Play 获取响应 Bundle 指示购买是否成功完成.
  4. 如果购买成功,配置产品并记住 购买 ID 已完成(这必须自动完成).
  5. 如果配置成功,则通过调用 consumePurchase 来消费购买
    (我以即发即弃"的方式执行此操作).
  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. 发送 getPurchases 请求以查询用户拥有的应用内商品.
  2. 如果发现任何消耗品,请检查开发者负载中的 购买 ID 是否存储在设备上.如果没有,请忽略该产品.
  3. 对于具有本地"购买 ID 的产品,检查 购买 ID 是否包含在完成列表.如果不是,请继续执行上述第 4 步,否则请继续执行上述第 5 步.
  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.

以下是在单个设备上出现问题的原因以及随后会发生的情况:

  • 如果购买从未开始或未完成,则不会向用户收费,应用会返回购买前状态,用户可以重试.未使用的 购买 ID 仍然在本地"列表中,但这应该只是一个相当小的内存泄漏",可以通过一些过期逻辑来修复.
  • 如果购买完成,但应用在第 4 步之前死机,当它重新启动时,它会找到待处理的购买(该产品仍报告为已拥有)并可以继续第 4 步.
  • 如果应用在第 4 步之后但在产品被消费之前死机,应用会在重启时找到待处理的购买,但知道忽略它,因为 购买 ID 是在完成列表中.该应用只是继续执行第 5 步.

在多设备情况下,任何其他设备将简单地忽略任何非本地待处理购买(报告为拥有的消耗品)作为购买 ID 不在该设备的本地列表中.

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.

一个问题是待定购买将阻止其他设备开始并行购买同一产品.因此,如果用户在他的手机上的第 2 步和第 5 步之间(即在购买完成之后,但在消费完成之前)的某个地方有不完整的交易,他将无法在他的平板电脑上购买相同的产品,直到应用完成第 5 步,即在手机上消费产品.

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.

通过将每个消耗品 SKU 的多个副本(可能是 5 个?)添加到 Google Play 并将第一个列表中的第 2 步更改为:

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. 使用 getBuyIntent购买 ID 为集合中的下一个可用 SKU启动购买流程开发者有效载荷.
  1. Launch a purchase flow for the next available SKU in the set with getBuyIntent with the purchase ID as the developer payload.

关于可破解性的说明(按黑客难度递增的顺序):

  1. 通过 Freedom APK<完成虚假购买/a> 或类似的:
    这些应用基本上是模拟 Google Play 商店来完成购买.要检测它们,需要验证签名 包含在购买收据中并拒绝未通过检查的购买,大多数应用程序不这样做(正确).大多数情况下问题已解决(参见第 4 点).
  2. 通过 Game Killer 或类似工具增加应用内消耗品的余额:
    这些应用程序会尝试找出您的应用程序在内存(或本地存储)中存储当前硬币或其他消耗品数量的位置,以直接修改数量.为了使这更难(即对于普通用户来说不可能),需要想出一种方法来存储帐户余额,而不是作为纯文本"整数,而是以某种加密方式或与一些校验和一起存储.大多数情况下问题已解决(参见第 4 点).
  3. 在正确的时间终止应用并破坏其本地存储:
    如果有人在其手机上购买了消费品并设法终止了该应用在配置产品之后但在消费之前(可能很难强制),然后他们可以修改其平板电脑上的本地存储以添加购买 ID 到本地列表,以便在每台设备上授予产品一次.或者,他们可能会破坏手机上已完成的购买 ID 列表,然后重新启动应用以获得两次奖励.如果他们在配置后但在消费产品之前再次设法杀死应用程序(现在只需将手机设置为飞行模式并删除 Google Play 商店缓存就很容易了),他们可以继续以这种方式窃取越来越多的产品.同样,对存储进行混淆或校验和会使这变得更加困难.
  4. 为应用程序反编译和开发补丁:
    当然,这种方法允许黑客对你的应用程序做任何他们想做的事情(包括破坏为缓解问题而采取的任何对策)第 1 点和第 2 点),并且很难完全预防.但是通过使用代码混淆(ProGuard)和关键采购管理代码的逻辑过于复杂(但可能会导致代码错误,因此这不一定是最好的主意).此外,代码的编写方式可以在不影响其功能的情况下修改其逻辑,以允许定期部署破坏任何可用补丁的替代版本.
  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.

请记住,我可能会忘记其他一些技巧.如果您知道,请发表评论.

结论:

总的来说,这不是最干净的解决方案,因为需要为每个消耗品维护多个并行 SKU,但到目前为止,我还没有想出更好的解决方案.

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.

所以,请分享您可能有的任何其他想法.+1 保证任何好的指针.:)

这篇关于解决 Google Play 的 Billing API v3 中消耗品的 API-purchase-logic-flaws(与使用 API v3 消耗品的所有人相关)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!

本文标题为:解决 Google Play 的 Billing API v3 中消耗品的 API-purchase-logic-flaws(与使用 API v3 消耗品的所有人相关)

基础教程推荐