如何在服务器端验证 android 应用的购买(google 在应用计费 v3 中播放)

How to verify purchase for android app in server side (google play in app billing v3)

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

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

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

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

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

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

您可以尝试使用 Purchases.subscriptions: get server-side. It takes packageName, subscriptionId and token as paramaters and requires authorization

Checks whether a user's subscription purchase is valid and returns its expiry time.

如果成功,此方法 returns 在响应正文中 Purchases.subscriptions resource

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

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

的主要权限

当用户进行应用内购买时,在客户端(即应用)本地缓存详细信息(令牌、订单 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 时,您也会向一些欺诈用户授予访问权限。 =20=]

换句话说:购买是有效的,直到被证明是欺诈或;欺诈直到证明有效?

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

中返回的 expiryTimeMillis

如果expiryTimeMillis是过去的,可以将premium标志设置为false。如果是在将来,请为新的 expiryTimeMillis.

重新安排一次

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

我回答这个问题

the network connection is down or my own server is down, user just paid the money in google play but I did not record the purchase in my server? What should I do, How can I deal with this situation.

情况是:

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

解决方案是:

在客户端,在显示 'Google Wallet' 按钮之前,您检查 'abc' 项目是否已被拥有。

  • 如果是,再次与服务器验证
  • 如果否,显示 'Google Wallet' 按钮。

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

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

  1. 设置您的 Google 项目 并访问 Google Play 用于您的 服务帐户,如 Marc 在此处的回答中所述

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

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

必要步骤:

  1. 创建一个 API 项目,从 API Access link 在你的 Google Play console

  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}

Play 开发者 API 平台服务接口的文档不是 link 易于查找的方式,对于某些人来说 很难找到。以下是我发现的流行平台的 link:

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

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

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

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

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

    -----开始 PUBLIC 键----- {private_key_id} -----结束 PUBLIC 关键-----

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

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

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

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

    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)

网络连接中断或我自己的服务器宕机,

你不必这样想。 客户知道自己消费的产品。因此,客户端可以将所有令牌发送回服务器。

只需 re-check 个带有生产 ID 和交易 ID 的代币。 服务器检查消费产品。

  1. 如果检查失败
  2. 使UI按钮客户端可以re-send令牌。
  3. 服务器 re-check 项目令牌。
  4. 完成了。