为什么在应用内购买中设置开发者负载很重要?

77

我正在使用应用内购买API的第3版,我有一个单一的、托管的、不可消耗的项目。我还没有在我的应用程序中发布此功能,因此我想在任何购买之前决定购买有效载荷内容。

"安全最佳实践"中:

在进行购买请求时设置开发人员有效载荷字符串

使用应用内购买版本3 API,您可以在将购买请求发送到Google Play时包括“开发人员有效载荷”字符串令牌。通常,这用于传递唯一标识此购买请求的字符串令牌。如果您指定了一个字符串值,Google Play会将此字符串与购买响应一起返回。随后,当您查询有关此购买的信息时,Google Play会将此字符串与购买详细信息一起返回。

您应该传递一个帮助您的应用程序识别进行购买的用户的字符串令牌,以便您稍后可以验证此用户的合法购买。对于可消耗的物品,您可以使用随机生成的字符串,但对于不可消耗的物品,您应该使用唯一标识用户的字符串。

从Google Play获得响应后,请确保验证开发人员有效载荷字符串与先前发送的购买请求中的令牌相匹配。作为进一步的安全预防措施,您应在自己的安全服务器上执行验证。

无论是对还是错,我已经决定采取“进一步的安全措施”,即建立一个服务器来执行购买验证。而且我也不会存储自己的购买记录 -- 我总是调用账单 API。那么,我真的需要进行有效负载验证吗?验证 API 本身确实在报告物品已购买之前验证了用户的身份,如果攻击者已经入侵了设备(无论是应用程序还是 Google Play API),我不认为在设备上对用户的身份进行额外检查有任何好处,因为这很容易被绕过。或者我没考虑到还有其他原因需要这样做吗?

8个回答

70

如果您不记录任何信息,就没有办法验证接收到的内容与发送的内容是否一致。因此,如果您添加了开发者负载,您可以信任它是合法的(如果签名验证成功的话这是一个合理的假设),或者不完全信任它只将其用作参考而不是验证许可状态等。例如,如果存储了用户电子邮件,则可以使用该值而无需再次要求他们输入,这样更加用户友好,但如果没有这个值,您的应用程序也不会出错。

个人认为,“最佳实践”部分很困惑,试图让您完成API应该真正执行的工作。由于购买与Google帐户绑定,而Play Store显然会保存此信息,因此它们应该只需在购买详情中提供此信息。获取正确的用户ID需要额外的权限,您不应该为弥补IAB API的缺陷而添加这些权限。

因此,简言之,除非您有自己的服务器和特殊的插件逻辑,否则请不要使用开发者负载。只要IAB v3 API工作正常(很不幸,目前仍存在相当大的问题),您就应该没问题。


4
在你最初回答之后的一年半时间里,你是否仍然持有同样的看法?我不确定 API 是否在幕后进行了任何更改......而且我现在也不能说我信任谷歌的文档。 - Alex Lockwood
6
我仍认为应该通过IAP提供透明的安全性,而不需要使用开发者负载。也许现在已经实现了,但我很久没有检查(甚至使用)它了。我的应用程序仍然主要使用原始的IAP,因为它提供了购买通知,我可以在自己的服务器上跟踪,而“新”的IAP则没有这个功能。 - Nikolay Elenkov
2
我认为有必要在服务器上进行验证,当然,如果您没有用户“登录”某种帐户标识符(您自己的或第三方),V3会使这变得非常困难。本来想尝试API V2但是...我们计划在2015年1月27日关闭应用内购买版本2服务,此后用户将无法通过版本2 API购买应用内物品和订阅。 我们强烈建议您在2014年11月之前将应用程序迁移到使用Version 3 API,以提供充足的时间供用户更新其应用程序到新版本。 - inteist
@Nikolay Elenkov,你现在有什么建议?我同意你的观点,整个最佳实践应该在Google这边。 - stuckedoverflow

30
您应该传递一个字符串令牌,帮助您的应用程序识别进行购买的用户...
如果您的应用程序提供自己的用户登录和身份验证,与手机连接的Google账户不同,那么您需要使用开发者负载将购买附加到其中一个创建购买的帐户。否则,某人可以在您的应用程序中切换帐户,并获得已购买商品的好处。
例如:
假设我们的应用程序有userA和userB的登录。而手机的Android Google账户是X。
1. userA,登录我们的应用程序并购买终身会员。购买详细信息存储在Google账户X下。 2. userA退出登录,userB登录我们的应用程序。现在,由于android google账户仍然为X,userB也获得了终身会员的好处。
为了避免这种滥用,我们将把购买绑定到一个帐户上。在上面的示例中,当userA进行购买时,我们将设置开发者负载为"userA"。因此,当userB登录时,负载将与登录的用户(userB)不匹配,我们将忽略购买。这样,userB就无法获得由userA执行的购买的好处。

好的回答。但是如果我们没有任何使用登录机制怎么办?那么我们就不使用有效载荷吗? - tasomaniac
7
经过思考这个用户ID问题一会儿,我认为你指出的可能是负载字符串的实际意图。但在谷歌文档中解释得非常不清楚。 - Pooks
我尝试了这个用例,但实际上这是无法解决的。正如你所提到的,购买是绑定到支付该商品的Google帐户的。如果用户A购买了终身会员,而用户B尝试在同一设备上使用不同的有效载荷购买相同的会员资格,则应用内购买响应为:“您已经拥有此项目”,因此您无法再次购买它。而且,您的verifyDevPayload()也将失败,因此用户实际上被死锁了...我认为开发人员有效载荷只适用于检查Google Play是否对请求返回了正确的响应...我看不到任何使用它的好处。 - keybee
3
这正是关键所在。在你提到的情况下,如果我们不使用开发者有效载荷(dev payload),当应用内购买返回“您已拥有此商品”时,我们就必须给用户B提供高级功能(因为我们无法知道是用户B首次购买)。 如果用户B必须在与用户A购买相同的设备上购买,那么用户B必须切换到自己的Google账户并进行购买。 - therealsachin
4
@therealsachin 我理解了,但是如果我在服务器上检查并阻止用户B,他将没有机会用另一个账号购买会员,因为Google Play 会先与主账号进行验证,所以同一设备上的两个用户将无法正常工作。然而只有当用户A出于某种原因需要另一个账号时,这才可能成为问题。他将永远无法使用相同的 Google 账号再次购买,即使使用不同的负载也不行。- 理想情况下,如果有新的负载,我应该能够再次购买订阅。 - keybee
显示剩余2条评论

21

还有另一种处理开发者负载的方法。如Nikolay Elenkov所说,要求用户ID并为用户配置附加权限对您的应用程序来说是太过繁琐的,因此这不是一个好的方法。那么让我们看看Google在最新版本的In-App Billing v3样例应用程序 TrivialDrive 中是怎么说的:

  • 警告:启动购买并在此处验证时本地生成随机字符串可能看起来是一个不错的方法,但如果用户在一台设备上购买商品然后在另一台设备上使用您的应用,则此方法将失败,因为在另一台设备上,您将无法访问最初生成的随机字符串。

因此,如果您要在另一台设备上验证已购买的项目,则随机字符串不是一个好主意,但他们仍然没有说这对于验证购买响应不是一个好主意。 我认为 - 仅使用开发者负载通过发送随机唯一字符串来验证购买,将其保存在首选项/数据库中,并在购买响应中检查此开发者负载。至于在Activity启动时查询库存(应用内购买)- 不要费心检查开发者负载,因为可能会在另一台设备上发生,您在那里没有存储该随机唯一字符串。这就是我看到的。


4
谢谢,我正在采用这种方法(我已经点赞了 :)),因为它听起来是一个很好的折衷方案,但我认为所描述的逻辑存在问题。如果在购买时有效负载验证失败,则Google Play仍然认为您拥有该物品,因此您只需重新启动应用程序,它将读取清单,并在没有有效负载验证的情况下向您提供该物品。一种修复方法是,在购买时有效负载验证失败后消耗该物品,但这样做会占用用户的资金,如果这种情况发生得不可避免地会导致讨厌的电子邮件...... - Georgie
如果您将设备ID作为有效载荷提供,您可以实现“按设备许可证”销售。因此,如果用户在一台设备上进行了购买,他将无法在另一台设备上使用该许可证。这对于销售应用程序的完整版本(例如防病毒软件)可能非常有用。 - bancer
阅读@Georgie的评论,否则你将花费很多时间来弄清楚这是一个坏主意。 - Halvor Holsten Strand

16

这取决于您如何验证developerPayload。有两种情况:远程验证(使用服务器)和本地验证(在设备上)。

服务器

如果您正在使用服务器进行developerPayload验证,则它可以是任意字符串,可以在设备和服务器上轻松计算。您应该能够识别执行请求的用户。假设每个用户都有相应的accountId,则developerPayload可以与purchaseId(SKU名称)组合计算,如下所示:

MD5(purchaseId + accountId)


设备

developerPayload不应该是用户的电子邮件。一个很好的例子是Google for Work服务,为什么你不应该使用电子邮件作为有效载荷。用户可以更改与账户关联的电子邮件地址。唯一不变的是accountId。在大多数情况下,电子邮件是可以接受的(例如,目前Gmail地址是不可变的),但请记住为未来进行设计。

多个用户可能会使用同一台设备,因此您必须能够区分谁是物品的所有者。对于设备验证,developerPayload是一个字符串,用于唯一识别用户,例如:

MD5(purchaseId + accountId)


结论

一般情况下,在这两种情况下,developerPayload 可能只是 accountId。对我来说,这看起来像是安全通过模糊性。 MD5(或其他哈希算法)和purchaseId只是使有效载荷更加随机的方式,而不明确显示我们正在使用帐户的ID。攻击者需要反编译应用程序才能检查计算方式。如果应用程序混淆得更好,那就更好了。

有效载荷不提供任何安全保障。可以轻松地使用“设备”方法欺骗它,并且在“服务器”检查中可以轻松地夺取它而不需要任何努力。请记得使用您在 Google 发布者帐户控制台中可用的公钥实施签名检查。

*关于使用帐户ID而不是电子邮件的必读博客文章。


1
什么是accountId?如何获取accountId?在多个Google Play账户之间切换时,是否可能保持相同的accountId?更多详情请参见https://github.com/googlesamples/android-play-billing/issues/2。 - Sarath Kumar

5
在有关IAB v3的Google IO视频中,作者本人简要地讨论了这个问题。它的作用是防止重放攻击,例如攻击者窃听流量、窃取包含成功购买信息的数据包,然后尝试在自己的设备上重放该数据包。如果您的应用程序在发布高级内容之前不通过开发人员负载(最好在您的服务器上)检查购买者的身份,那么攻击者将会成功。由于数据包完整,签名验证无法检测到这一点。
在我看来,这种保护对于像部落冲突这样具有在线账户连接的应用程序非常理想(负载自然而然地出现,因为您必须识别用户),特别是当黑客入侵多人游戏玩法并产生远达影响的情况下,而不仅仅是简单的盗版问题。相比之下,如果客户端apk上已经存在可以解锁高级内容的黑客,则此保护功能不是很有用。
(如果攻击者试图欺骗负载,则签名验证应该失败)。

我可以在Google登录和实时数据库中使用Firebase,其中包含account_id和developer_payload字段,并在用户打开应用程序时检查它们(同时提供离线功能的附加共享首选项)吗? - Dhaval Jotaniya

5
2018年底更新:官方的Google Play计费库故意不公开developerPayload字段。来自这里的消息如下:

developerPayload是一个遗留字段,为了保持与旧实现的兼容性而保留,但是如购买应用内计费产品页面所述(https://developer.android.com/training/in-app-billing/purchase-iab-products.html),当完成与应用内计费相关的任务时,该字段并不总是可用的。 由于该库旨在代表最新的开发模型,我们决定不支持在我们的实现中使用developerPayload,并且我们没有将该字段包含到库中的计划。

如果您依赖于developerPayload对应用内计费逻辑的任何重要实现,我们建议您更改此方法,因为该字段将在某个时间点(或很快)被弃用。 推荐的方法是使用您自己的后端来验证和跟踪有关订单的重要细节。有关更多详细信息,请查看安全性和设计页面(https://developer.android.com/google/play/billing/billing_best_practices.html)。


这对我来说解决了问题...它已经被弃用,因此不应该使用。 - jwitt98

3
我对此有些困惑。由于一个Google Play账户只能拥有一个“被管理”的项目,但可能有多个设备(我有三个),因此上面的评论说你出售“每个设备”的产品是行不通的……他们只能在第一个设备上使用,而其他设备则无法使用……如果购买高级升级版,则可以在您的所有手机/平板电脑上使用。
我厌恶获取用户的电子邮件地址的想法,但我确实找不到其他可靠的方法。因此,我选择在帐户列表中匹配“google.com”的第一个帐户(是的,在清单中添加权限),然后立即对其进行哈希处理,以便不能再用作电子邮件地址,但提供了一个“足够独特”的标记。这就是我发送的开发者有效负载。由于大多数人使用他们的Google Play id激活设备,所以很有可能所有三个设备都会获得相同的标记(在每个设备上使用相同的哈希算法)。
它甚至在具有多个“用户”的KitKat上工作。(我的开发者id位于一个用户上,我的测试id位于另一个用户上,并且每个用户都在自己的沙箱中。)
我已在六个设备上进行了测试,共有3个用户,每个用户的设备都返回相同的哈希值,不同的用户都有不同的哈希值,这符合准则。
在任何时候,我都没有存储用户的电子邮件地址,它从代码传递到获取帐户名称的哈希函数中,只有哈希保存在堆中。
可能仍然存在更好的解决方案,可以进一步尊重用户的隐私,但到目前为止,我还没有找到。一旦应用程序发布,我将清楚地描述我如何使用用户的电子邮件地址,并在隐私政策中概述。

6
这很好,但需要ACCOUNTS权限,这在这里是不必要的过度。天哪,为什么谷歌不能让用于购买的账户的哈希可访问... 这将是最清晰的解决方案。现在它只是明显的“损坏”。 - inteist
我之前做过类似的事情,但现在在Android 6上,应用程序必须要求用户授予联系人权限,这对于第一次启动应用程序的用户来说是一个可怕的体验。也许我会实现一个恢复购买选项。此外,如果你获取第一个google.com帐户,如果用户在手机上有多个帐户并重新排列帐户(例如,删除帐户A然后稍后再添加它,但现在它不是第一个帐户),就会遇到问题。虽然很少发生,但确实会发生。所以你需要使用账户选择器!呕! - snark

1
这通常与产品定义(您的应用程序)有关。例如,对于订阅的情况,同一用户是否能够在拥有的所有设备上使用订阅?如果答案是肯定的,我们没有检查有效载荷。
对于消耗品。假设您的应用程序中的购买会给您10个虚拟硬币。用户是否能够在不同的设备上使用这些硬币?比如一个设备上4个,另一个设备上6个?如果我们只想在进行购买的设备上工作,我们必须检查有效载荷,例如使用自动生成的字符串和本地存储。
基于这些问题,我们必须决定如何实现有效载荷检查。
敬礼,
Santiago

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接