【问题标题】:How to verify purchase for android app in server side (google play in app billing v3)如何在服务器端验证 android 应用的购买(应用计费 v3 中的谷歌播放)
【发布时间】:2023-04-04 05:55:01
【问题描述】:

我有一个简单的应用程序(需要用户使用帐户登录)。我为付费用户提供了一些高级功能,例如更多新闻内容。

我需要记录用户是否在我的服务器数据库中购买了该商品。当我向用户的设备提供数据内容时,我可以检查用户的状态,并为付费用户提供不同的内容。

我查看了谷歌官方提供的 Trivialdrive 示例,它没有提供任何用于服务器端验证的示例代码,这是我的问题。

  1. 我发现该示例使用我的应用程序的公钥来验证购买,看起来不太好,我想我可以将验证过程移动到我的服务器结合用户登录凭据查看用户购买是否完成,然后更新数据库。
  2. 还有purchase API可以用来查询,我需要的是把用户的purchaseToken传给服务器。

我不确定我应该采取什么方法来验证用户的购买,并在我的数据库中标记用户的状态,也许两者兼而有之?

恐怕有一种情况,如果用户从 google play 购买了这个项目,但是由于某种原因,就在那个时候,当我的应用程序向我的服务器发起验证时,网络连接断开或我自己的服务器宕机了,用户刚刚在 google play 中支付了钱,但我没有在我的服务器中记录购买?我该怎么办,我该如何处理这种情况。

【问题讨论】:

标签: android in-app-purchase in-app-billing


【解决方案1】:

您可以尝试使用Purchases.subscriptions: get 服务器端。它以 packageName、subscriptionId 和 token 作为参数,需要authorization

检查用户的订阅购买是否有效并返回其 过期时间。

如果成功,此方法在响应正文中返回一个Purchases.subscriptions resource

【讨论】:

【解决方案2】:

听起来您正在寻找一种方法来检查用户是否在其帐户上启用了高级功能,所以这就是我要开始的地方;

确保您的数据库中有某种标志,指示用户是否具有高级功能,并在请求帐户信息时将其包含在 API 响应负载中。此标志将是您对“高级功能”的主要授权。

当用户进行应用内购买时,在客户端(即应用)本地缓存详细信息(令牌、订单 ID 和产品 ID),然后将其发送到您的 API。

然后,您的 API 应将 purchaseToken 发送到 Google Play Developer API 以进行验证。

这里可能会发生一些事情:

  1. 收据有效,您的 API 以 200 Ok 状态码响应客户端
  2. 收据无效,您的 API 以 400 Bad Request 状态代码响应客户端
  3. Google Play API 已关闭,您的 API 响应 502 Bad Gateway 状态代码

在 1. 或 2.(2xx 或 4xx 状态代码)的情况下,您的客户端会清除购买详细信息的缓存,因为它不再需要它,因为 API 已指示已收到它。

成功验证后(案例 1.),您应该为用户将 premium 标志设置为 true。

如果是 3.(5xx 状态代码)或网络超时,客户端应继续尝试,直到从您的 API 收到 2xx 或 4xx 状态代码。

根据您的要求,您可以让它在再次发送之前等待几秒钟,或者在应用再次启动或退出后台时(如果应用缓存中存在购买详细信息)将详细信息发送到您的 API。

这种方法应该处理网络超时、服务器不可用等问题。

现在您需要考虑几个问题:

购买后应该立即发生什么?应用程序应该等到验证成功后再提供优质内容,还是应该暂时授予访问权限并在验证失败时将其移除?

授予对高级功能的暂定访问权限可以使您的大多数用户的流程顺利进行,但在您的 API 验证 purchaseToken 时,您也将授予许多欺诈用户的访问权限。

换句话说:购买在被证明存在欺诈之前有效,或者;在被证明有效之前是欺诈性的?

为了确定用户在订阅期到期时是否仍有有效订阅,您需要在purchaseToken 上安排重新验证,以便在expiryTimeMillis 上运行result.

如果 expiryTimeMillis 是过去的,您可以将 premium 标志设置为 false。如果是在未来,请为新的expiryTimeMillis 重新安排。

最后,为确保用户拥有(或不拥有)高级访问权限,您的应用应在应用启动或退出后台时向您的 API 查询用户详细信息。

【讨论】:

  • 对于付费应用,我将如何从谷歌获得收据?
  • 嗨!没有办法在谷歌访问订阅历史?存储purchaseToken时应用崩溃,如何防止丢失已购买cuscription的事实?
  • 我有类似的问题.. 而不是让应用程序将令牌发送到 api 不会更可靠地指示 google 开发人员服务器通过将推送通知直接发送到我的 api 来执行此操作?
  • 对于已取消的订阅,如果使用相同的旧购买令牌进行验证,Google Play Developer API 在取消后仍将返回 200。
  • 所以对于订阅,您建议在服务器上第一次调用后,我们存储购买令牌和产品 ID,并在何时安排另一个验证调用(重新运行相同的请求) expiryTimeMillis 发生了吗?这就是我们应该验证订阅有效性的方式吗? Android有关于如何做到这一点的任何指导方针吗? Apple 获得了一个关于它的 WWDC 视频,非常清楚地解释了它的良好实践,但找不到关于 Play Store 的太多信息。
【解决方案3】:

我回答这个问题

网络连接断开或我自己的服务器断开,用户只是 在 google play 中支付了钱,但我没有在我的 服务器?我该怎么办,我该如何处理这种情况。

情况是:

用户使用 google play 服务购买 'abc' 项目 -> 返回 OK -> 由于某些原因(例如没有 Internet 连接)无法与服务器验证。

解决办法是:

在客户端,在显示“Google 电子钱包”按钮之前,您会检查“abc”项目是否已被拥有。

  • 如果是,再次与服务器验证
  • 如果没有,请显示“Google 电子钱包”按钮。

Purchase purchase = mInventory.getPurchase('abc');

if (purchase != null) // Verify with server 

else // show Google Wallet button

https://developer.android.com/google/play/billing/billing_reference.html#getSkuDetails

【讨论】:

  • 我不明白为什么在服务器上验证比在应用程序上验证更安全。归根结底,解锁功能的是应用程序,因此仍然可以删除或反转应用程序中检查服务器响应是否“正常”的代码
  • @GianlucaGhettini 因为有时服务器是提供购买的服务而不是应用程序,此外,应用程序可能会被逆向工程,然后经过一番努力,验证过程可能会被黑客入侵。
【解决方案4】:

使用Google API Client Library for PHP的完整示例:

  1. 设置您的 Google 项目 并为您的服务帐户 访问 Google Play,如 Marc 在此处的回答 https://stackoverflow.com/a/35138885/1046909 中所述。

  2. 安装库:https://developers.google.com/api-client-library/php/start/installation

  3. 现在您可以通过以下方式验证收据:

    $client = new \Google_Client();
    $client->setAuthConfig('/path/to/service/account/credentials.json');
    $client->addScope('https://www.googleapis.com/auth/androidpublisher');
    $service = new \Google_Service_AndroidPublisher($client);
    $purchase = $service->purchases_subscriptions->get($packageName, $productId, $token);
    

    之后 $purchase 是 Google_Service_AndroidPublisher_SubscriptionPurchase

    的实例
    $purchase->getAutoRenewing();
    $purchase->getCancelReason();
    ...
    

【讨论】:

  • 这不起作用,我不断收到 (401) Login Required 和 setAuthConfig 不接受服务帐户凭据 json
  • 这个对我有用 putenv('GOOGLE_APPLICATION_CREDENTIALS=credentials.json'); $client = new Google_Client(); $client->useApplicationDefaultCredentials(); $client->addScope('googleapis.com/auth/androidpublisher'); $service = new Google_Service_AndroidPublisher($client); $purchase = $service->purchases_products->get($packageName, $productId, $token); var_dump($purchase);
  • 这个东西是在应用内计费的情况下。如果每当用户从 Play 商店而不是 inapp 购买我的应用程序时,我想将 orderId 获取到我的数据库怎么办?
  • stackoverflow.com/questions/48662787/… 请看这个问题。我正在寻找这个问题的答案。这也有活跃的赏金
  • @MingalevME 如果令牌格式无效并且 PHP 出现致命错误,我该如何捕捉这个错误?
【解决方案5】:

关于这方面的文档令人困惑且奇怪地冗长,其中包含几乎无关紧要的内容,而实际上重要的文档几乎没有链接并且很难找到。这应该在可以运行 google api 客户端库的大多数流行服务器平台上运行良好,包括 Java、Python、.Net 和 NodeJS 等。注意:我只测试了如下所示的 Python api 客户端。

必要步骤:

  1. 通过 Google Play 控制台中的 API 访问链接创建一个 API 项目

  2. 创建一个新的服务帐户,保存生成的 JSON 私钥。您需要将此文件带到您的服务器。

  3. 在 Play 控制台的服务帐户部分按完成以刷新并授予对服务帐户的访问权限

  4. https://developers.google.com/api-client-library为你的服务器平台获取一个google api客户端库

  5. 使用您特定平台的客户端库构建服务接口并直接读取您的购买验证结果。

您确实不需要需要处理授权范围、进行自定义请求调用、刷新访问令牌等。api 客户端库会处理所有事情。下面是一个验证订阅的 python 库使用示例:

首先,像这样在你的 pipenv 中安装 google api 客户端:

$ pipenv install google-api-python-client

然后您可以使用私钥 json 文件设置 api 客户端凭据以验证服务帐户。

credentials = service_account.Credentials.from_service_account_file("service_account.json")

现在您可以直接使用库验证订阅购买或产品购买。

#Build the "service" interface to the API you want
service = googleapiclient.discovery.build("androidpublisher", "v3", credentials=credentials)

#Use the token your API got from the app to verify the purchase
result = service.purchases().subscriptions().get(packageName="your.app.package.id", subscriptionId="sku.name", token="token-from-app").execute()
#result is a python object that looks like this ->
# {'kind': 'androidpublisher#subscriptionPurchase', 'startTimeMillis': '1534326259450', 'expiryTimeMillis': '1534328356187', 'autoRenewing': False, 'priceCurrencyCode': 'INR', 'priceAmountMicros': '70000000', 'countryCode': 'IN', 'developerPayload': '', 'cancelReason': 1, 'orderId': 'GPA.1234-4567-1234-1234..5', 'purchaseType': 0}

播放开发者 API 的平台服务接口的文档链接起来并不容易,对于某些人来说,它是彻头彻尾的很难找到。以下是我找到的流行平台的链接:

Python | Java | .NET | PHP | NodeJS (Github TS) | Go (Github JSON)

【讨论】:

  • 同意,文档太糟糕了...关于如何使用 Firebase (Firestore) 和 Cloud 功能作为后端来做到这一点的任何想法?
  • 如果您的云函数在 NodeJS 中,那么您或许可以使用上面的 NodeJS 链接来让 API 客户端库正常工作?
  • 很难找到,因为他们想推广会产生费用的 Cloud PubSub
【解决方案6】:

Marc Greenstock 的回答绝对有启发性,但有几件事需要注意,但我花了很长时间才弄清楚(至少比我预期的要多得多):

  1. 我必须在服务帐户设置中选中“启用 G Suite 域范围委派”。没有这个我一直收到这个错误:“当前用户没有足够的权限来执行请求的操作” Image with Enable G Suite Domain-wide Delegation option checked

  2. 出于测试目的,您可以为您的服务帐户here 创建一个 JWT 令牌,只是不要忘记选择 RS256 算法。

  3. 公钥是您下载的 JSON 文件中的“private_key_id”。它还具有以下格式:

    -----开始公钥----- {private_key_id} -----结束公钥-----

  4. 私钥是您下载的 JSON 文件中的“private_key”

  5. JWT 生成所需的声明描述为here

  6. 对 JWT 令牌到底是什么以及它是如何组装的感到困惑?不要害羞,检查this link。很可能你和我一样,花了很长时间才费心去寻找它到底是什么,它(方式)比看起来简单。

【讨论】:

    【解决方案7】:

    我在使用建议的 google API python 库时遇到了一些严重的问题,但是从头开始实现通信并不难。 首先,您必须按照所有答案中的说明在Google Play Console 创建一个服务帐户,并获取包含私钥的 JSON 文件。将其保存到您的服务器。 然后使用以下代码。无需获取 google API 客户端库。你只需要以下(非常常见的)python库RequestsPycrypto

        import requests
        import datetime
        import json
        import base64
        from Crypto.Signature import PKCS1_v1_5 as Signature_pkcs1_v1_5
        from Crypto.Hash import SHA256 
        from Crypto.PublicKey import RSA
    
        jwtheader64 = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
        #SERVICE_ACCOUNT_FILE: full path to the json key file obtained from google
        with open(SERVICE_ACCOUNT_FILE) as json_file:
            authinfo = json.load(json_file)
    
        packageName = #your package name
        product = #your inapp id
        token = #your purchase token
        
        #create the JWT to use for authentication
        now = datetime.datetime.now()
        now1970 = (now - datetime.datetime(1970,1,1)).total_seconds()
        jwtclaim = {"iss":authinfo["client_email"],"scope":"https://www.googleapis.com/auth/androidpublisher","aud": "https://oauth2.googleapis.com/token","iat":now1970,"exp":now1970+1800,"sub":authinfo["client_email"]}
        jwtclaimstring = json.dumps(jwtclaim).encode(encoding='UTF-8')
        jwtclaim64 = base64.urlsafe_b64encode(jwtclaimstring).decode(encoding='UTF-8')
        tosign = (jwtheader64+"."+jwtclaim64).encode(encoding='UTF-8')
    
        #sign it with your private key
        private = authinfo["private_key"].encode(encoding='UTF-8')
        signingkey = RSA.importKey(private)
        signer = Signature_pkcs1_v1_5.new(signingkey)
        digest = SHA256.new()
        digest.update(tosign)
        signature = signer.sign(digest)
        res = base64.urlsafe_b64encode(signature).decode(encoding='UTF-8')
    
        #send it to Google authentication server to obtain your access token
        headers = {'Content-Type': 'mapplication/x-www-form-urlencoded'}
        payload = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion="+jwtheader64+"."+jwtclaim64+"."+res
        r = requests.post("https://oauth2.googleapis.com/token",headers=headers,data=payload)
        if r.status_code == 200:
            authdata = json.loads(r.text)
            accesstoken = authdata['access_token']
            bearerheader = {'Authorization':'Bearer '+authdata['access_token']}
            #Now you have at last your authentication token, so you can use it to make calls. In this example we want to verify a subscription
            url = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/"+packageName+"/purchases/subscriptions/"+product+"/tokens/"+token
            subscription = requests.get(url,headers=bearerheader)
    

    【讨论】:

      猜你喜欢
      • 2019-09-23
      • 2019-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-27
      • 2012-01-13
      相关资源
      最近更新 更多