Google Play应用内购买版本3的服务器端验证

40

我无法直接找到如何在向用户提供可下载内容之前,服务器上验证应用内购买的答案。

我使用应用内结算版本3。我使用基于TrivialDrive示例代码中的IabHelper类的代码购买托管产品。一切都很好,购买成功完成,我收到完整的Purchase对象和以下原始JSON数据:

{
    "orderId":"12999763169054705758.1364365967744519",
    "packageName":"my package name",
    "productId":"77",
    "purchaseTime":1366217534000,
    "purchaseState":0,
    "purchaseToken":"utfwimslnrrwvglktizikdcd.AO-J1OwZ4l5oXz_3d2SAWAAUgFE3QErKoyIX8WuSEnBW26ntsyDmlLgoUd5lshqIY2p2LnlV4tpH4NITB4mJMX98sCtZizH7wGf6Izw3tfW_GflJDKFyb-g"
}

据我理解,我需要将购买令牌和一个被称为签名的东西传递给服务器。然后,服务器使用私钥来验证购买。这是正确的吗?如果是这样,在哪里可以获取签名,并且是否真的没有关于服务器端验证购买的体面文档?


19
在向社群提问时,记住一件事情:不要在问题中透露敏感信息。所谓敏感信息包括密码、订单号等交易信息。请使用虚构数据替换它们。 - Ankit
你有找到任何可用的服务器端验证示例吗? - Dr.jacky
如何使用并从Google提取上述JSON数据?我想看到关于此的实际PHP代码。 - creator
签名验证是使用公钥而不是私钥完成的。 - Gianluca Ghettini
5个回答

22
where do I get the signature from ?

请查看官方文档

文档中指出在onActivityResult()方法内您可以获取下列数据,就像示例中所示一样,

    @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
   if (requestCode == 1001) {           
      int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
      String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
      String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");//this is the signature which you want

      if (resultCode == RESULT_OK) {
         try {
            JSONObject jo = new JSONObject(purchaseData);//this is the JSONObject which you have included in Your Question right now
            String sku = jo.getString("productId");
            String purchaseToken = jo.getString("purchaseToken");
           //you need to send sku and purchaseToken to server for verification
          }
          catch (JSONException e) {
             alert("Failed to parse purchase data.");
             e.printStackTrace();
          }
      }
   }
}

服务器端验证: 请参阅 官方文档

如前所述,客户端应用将向服务器API发送skupurchaseToken。 服务器将接收这些值并使用Android Publish API执行购买验证:

服务器可以通过添加必要的参数调用以下GET请求:

https://www.googleapis.com/androidpublisher/v2/applications/packageName/purchases/products/productId/tokens/token

其中,
packageName = 客户端应用程序的包名
productId = 来自客户端应用程序的sku
token = 来自客户端应用程序的purchaseToken

服务器将以下列格式返回一个JSONObject对象:

{
  "kind": "androidpublisher#productPurchase",
  "purchaseTimeMillis": long,
  "purchaseState": integer,
  "consumptionState": integer,
  "developerPayload": string,
  "orderId": string,
  "purchaseType": integer
}

在这里,purchaseState = 0 表示有效购买

希望这对您有所帮助!!


1
我简直不敢相信我竟然忽略了这个。这是一个RTFM的典型案例。谢谢! - britzl
1
那么如何将上述JSON数据传递到我的PHP服务器代码中,并从那里使用(验证)它呢? - creator
6
这不是服务器端验证。 - Jemshit Iskenderov
Google建议在安全服务器上验证签名,但你的代码没有验证任何签名。开发者应该提供这个服务器。 - from56
@PinkeshDarji https://dev59.com/21YM5IYBdhLWcg3wxSR_#48531877 - from56
显示剩余6条评论

7

我对减少应用内购买欺诈的小贡献

在你的Android代码中,通过外部服务器验证签名:

verifySignatureOnServer()

  private boolean verifySignatureOnServer(String data, String signature) {
        String retFromServer = "";
        URL url;
        HttpsURLConnection urlConnection = null;
        try {
            String urlStr = "https://www.example.com/verify.php?data=" + URLEncoder.encode(data, "UTF-8") + "&signature=" + URLEncoder.encode(signature, "UTF-8");

            url = new URL(urlStr);
            urlConnection = (HttpsURLConnection) url.openConnection();
            InputStream in = urlConnection.getInputStream();
            InputStreamReader inRead = new InputStreamReader(in);
            retFromServer = convertStreamToString(inRead);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }

        return retFromServer.equals("good");
    }

convertStreamToString()

 private static String convertStreamToString(java.io.InputStreamReader is) {
        java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
        return s.hasNext() ? s.next() : "";
    }

在 Web 托管的根目录下找到 verify.php 文件。

<?php
// get data param
$data = $_GET['data'];

// get signature param
$signature = $_GET['signature'];

// get key
$key_64 = ".... put here the base64 encoded pub key from google play console , all in one row !! ....";



$key =  "-----BEGIN PUBLIC KEY-----\n".
        chunk_split($key_64, 64,"\n").
       '-----END PUBLIC KEY-----';   
//using PHP to create an RSA key
$key = openssl_get_publickey($key);


// state whether signature is okay or not
$ok = openssl_verify($data, base64_decode($signature), $key, OPENSSL_ALGO_SHA1);
if ($ok == 1) {
    echo "good";
} elseif ($ok == 0) {
    echo "bad";
} else {
    die ("fault, error checking signature");
}

// free the key from memory
openssl_free_key($key);

?>

注意:

  • 如果您不加密URL,那么在解压缩的app apk中进行简单文本搜索就可以轻松找到URL。

  • 最好将php文件名、url参数、好/坏反应更改为没有意义的东西。

  • 如果不在单独的线程中运行verifySignatureOnServer(),则会抛出主线程异常。

希望这对您有所帮助...


String data, String signature 是什么? - user25
1
getPurchases() 返回一个 Bundle 对象,其中 owned items、data 和 signature 分别是“INAPP_PURCHASE_DATA_LIST”键和“INAPP_DATA_SIGNATURE_LIST”键的成员。 - from56
你每次都验证已购买的物品吗?我的意思是,你总是需要互联网连接来进行验证。 - user25
每8小时在AsyncTask中调用getPurchases(),如果UrlConnection失败,则使用java.security验证签名。 - from56
最好使用Https,此外在Android 9及更高版本中不允许明文流量,因此任何使用纯文本Http的代码都将停止在Android 9上工作。 - from56
显示剩余4条评论

4

1. 使用composer安装Google客户端PHP库

composer require google/apiclient:"^2.0"
  1. 创建服务账号,它会给你一个 JSON 文件并将其保存为 credentials.json。

enter image description here

  1. start code

    require $_SERVER['DOCUMENT_ROOT'] . '/client/vendor/autoload.php';
    
    $packageName='Your Package name'; //com.example.blahblah
    $productId='your_product_ID';
    $token='Purchase_Token_Form_Payment';
    
    $client = new \Google_Client();
    $client->setAuthConfig('credentials.json');
    $client->addScope('https://www.googleapis.com/auth/androidpublisher');
    
    $service = new \Google_Service_AndroidPublisher($client);
    $purchase = $service->purchases_products->get($packageName, $productId, $token);
    
    //echo $purchase['purchaseState'];
    //echo '<br>';
    echo json_encode($purchase);
    

结果会是这样的

{"acknowledgementState":1,"consumptionState":1,"developerPayload":"","kind":"androidpublisher#productPurchase","orderId":"GPA.3342-8146-5668-57982","productId":null,"purchaseState":0,"purchaseTimeMillis":"1586978561493","purchaseToken":null,"purchaseType":0,"quantity":null}

你好,我喜欢使用官方库来处理认证的方法。问题是:服务账户需要长什么样子?即需要授予哪些权限以及在哪里授予?我记得在 Google 文档中看到过,但现在找不到了...非常感谢! - Daniele Muscetta

3
这是一个比较老的问题,但我希望我的回答能够帮到一些人。
你需要在客户端验证签名,然后将purchaseToken传递到服务器端,服务器将会与Google服务器联系并获取有关购买的所有必要信息,例如purchaseStateconsumptionState
参考链接:https://developers.google.com/android-publisher/api-ref/purchases/products

1
这部分是正确的。Google实际上建议在可能的情况下在服务器端执行签名验证。这样,您就不会在Android应用中公开您的公钥。 - user2213590
1
你如何在客户端验证签名。 - Likith Ts
@Eran,请问你能否在这里发布一个有关服务器端签名验证的 Google 文档链接? - Petr

0

这是一个古老的问题,但我认为它仍然相关。

我的小贡献。购买对象扩展了一个新参数“已确认”,现在它看起来像:

{
  "orderId":"12999763169054705758.1364365967744519",
  "packageName":"my package name",
  "productId":"77",
  "purchaseTime":1366217534000,
  "purchaseState":0,
  "purchaseToken":"utfwimslnrrwvglktizikdcd.AO-J1OwZ4l5oXz_3d2SAWAAUgFE3QErKoyIX8WuSEnBW26ntsyDmlLgoUd5lshqIY2p2LnlV4tpH4NITB4mJMX98sCtZizH7wGf6Izw3tfW_GflJDKFyb-g",
  "acknowledged":true
}

因此,在检查签名时添加这个额外参数时要小心。


1
它不包含autoRenewing字段吗? - Kishan Vaishnav
@KishanVaishnav 或许只针对订阅而非一次性产品? - Daniele Muscetta

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