如何在本地最好地保存InApp购买状态?

时间:2022-07-10 05:02:24

I'm on the edge of finishing my first app, and one last remaining thing is to implement IAP billing, so that's why I am currently reading quite a lot about the topic (including security concerns like encryption, obfuscation and stuff).

我正处于完成我的第一个应用程序的边缘,剩下的最后一件事就是实现IAP计费,这就是为什么我目前正在阅读相关主题(包括加密,混淆和东西等安全问题)。

My app is a free version, with the ability to upgrade to full verison via IAP, so there would be just one managed purchase item "premium". I have a few questions about this:

我的应用程序是免费版本,能够通过IAP升级到完整版本,因此只有一个托管购买项目“溢价”。我有几个问题:

In the Google IAP API example (trivialdrivesample), there's always the IAP check in MainActivity to see if the user bought the premium version, done via

在Google IAP API示例(trivialdrivesample)中,总是在MainActivity中检查IAP以查看用户是否购买了高级版本,通过

mHelper.queryInventoryAsync(mGotInventoryListener);

mHelper.queryInventoryAsync(mGotInventoryListener);

My first concern: This does mean that the user always needs to have an internet/data connection at app-startup, to be able switch to the premium version right? What if the user doesn't have an internet connection? He would go with the lite version I guess, which I would find annoying.

我的第一个问题:这是否意味着用户始终需要在应用启动时拥有互联网/数据连接,才能切换到高级版本?如果用户没有互联网连接怎么办?他会选择我想的Lite版本,我觉得这很烦人。

So I thought about how to save the isPremium status locally, either in the SharedPrefs or in the app database. Now, I know you can't stop a hacker to reverse engineer the app, no matter what, even so because I don't own a server to do some server-side validation.

所以我想到了如何在SharedPrefs或app数据库中本地保存isPremium状态。现在,我知道你不能阻止黑客对应用程序进行逆向工程,无论如何,即便如此,因为我没有服务器来进行服务器端验证。

Nevertheless, one simply can't save an "isPremium" flag somewhere, since that would be too easy to spot.

然而,人们根本无法在某处保存“isPremium”标志,因为这样太容易被发现。

So I was thinking about something like this:

所以我在考虑这样的事情:

  • User buys Premium
  • 用户购买Premium
  • App gets the IMEI/Device-ID and XOR encodes it with a hardcoded String key, saves that locally in the app database.
  • 应用程序获取IMEI / Device-ID,XOR使用硬编码的String键对其进行编码,并将其保存在应用程序数据库中。

Now when the user starts the app again:

现在,当用户再次启动应用程序时:

  • App gets encoded String from database, decodes it and checks if decodedString == IMEI. If yes -> premium
  • App从数据库中获取编码的String,对其进行解码并检查decodeString == IMEI。如果是 - >溢价
  • If no, then the normal queryInventoryAsync will be called to see if the user bought premium.
  • 如果不是,则将调用正常的queryInventoryAsync以查看用户是否购买了premium。

What do you think about that approach? I know it's not supersecure, but for me it's more important that the user isn't annoyed (like with mandatory internet connection), than that the app will be unhackable (which is impossible anyway). Do you have some other tips?

您如何看待这种方法?我知道这不是超级固定,但对我来说,更重要的是用户不会烦恼(比如强制性互联网连接),而不是应用程序将无法解决(无论如何这是不可能的)。你有其他一些提示吗?

Another thing, which I currently don't have a clue about, is how to restore the transaction status when the user uninstalls/reinstalls the app. I know the API has some mechanism for that, and aditionally my database can be exported and imported through the app (so the encoded isPremium flag would be exportable/importable as well). Ok, I guess that would be another question, when the time is right ;-)

另一件事,我目前还没有任何线索,是如何在用户卸载/重新安装应用程序时恢复事务状态。我知道API有一些机制,并且我的数据库可以通过应用程序导出和导入(因此编码的isPremium标志也可以导出/导入)。好的,我猜这将是另一个问题,当时机成熟时;-)

Any thoughts and comments to this approach are welcome, do you think that's a good solution? Or am I missing something/heading into some wrong direction?

对这种方法的任何想法和评论都是受欢迎的,你认为这是一个很好的解决方案吗?或者我错过了什么/朝着错误的方向前进?

3 个解决方案

#1


16  

I too was making the same investigations, but during my testing I figured out that you do not need to store it, as Google do all the caching you need and I suspect (though I have not investigated it) that they are doing so as securely as possible (seeing as it in their interest too!)

我也在做同样的调查,但是在我的测试中我发现你不需要存储它,因为谷歌会做你需要的所有缓存,我怀疑(尽管我没有调查过)他们这样做是安全的尽可能(看到他们感兴趣的!)

So here is what i do

所以这就是我的所作所为

// Done in onCreate
mHelper = new IabHelper(this, getPublicKey());

mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
      if (!result.isSuccess()) {
         // Oh noes, there was a problem.
         Log("Problem setting up In-app Billing: " + result);
      } else {
         Log("onIabSetupFinished " + result.getResponse());
         mHelper.queryInventoryAsync(mGotInventoryListener);
     }
    }
});

// Called by button press
private void buyProUpgrade() {
    mHelper.launchPurchaseFlow(this, "android.test.purchased", 10001,   
           mPurchaseFinishedListener, ((TelephonyManager)this.getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId());
}

// Get purchase response
private IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result, Purchase purchase) 
    {
       if (result.isFailure()) {
          Log("Error purchasing: " + result);
          return;
       }      
       else if (purchase.getSku().equals("android.test.purchased")) {
      Log("onIabPurchaseFinished GOT A RESPONSE.");
              mHelper.queryInventoryAsync(mGotInventoryListener);
      }
    }
};

// Get already purchased response
private IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
    public void onQueryInventoryFinished(IabResult result,
       Inventory inventory) {

       if (result.isFailure()) {
         // handle error here
           Log("Error checking inventory: " + result); 
       }
       else {
         // does the user have the premium upgrade?        
         mIsPremium = inventory.hasPurchase("android.test.purchased");        
         setTheme();

         Log("onQueryInventoryFinished GOT A RESPONSE (" + mIsPremium + ").");
       }
    }
};

So what happens here?

那么这里发生了什么?

The IAB is set up and calls startSetup, on a successful completion (as long as it has been run once with an internet connection and is set up correctly it will always succeed) we call queryInventoryAsync to find out what is already purchased (again if this has been called while online it always works while offline).

成功完成IAB设置并调用startSetup(只要它已通过Internet连接运行一次并且设置正确,它将始终成功)我们调用queryInventoryAsync来查找已购买的内容(如果这样的话)已经在线上调用它在离线时总能正常工作)。

So if a purchase is completed successfully (as can only be done while online) we call queryInventoryAsync to ensure that it has been called while online.

因此,如果购买成功完成(只能在线完成),我们调用queryInventoryAsync以确保在线时调用它。

Now there is no need to store anything to do with purchases and makes your app a lot less hackable.

现在没有必要存储任何与购买有关的东西,并且使你的应用程序不那么容易破解。

I have tested this many ways, flight mode, turning the devices off an on again and the only thing that messes it up is clearing data in some of the Google apps on the phone (Not likely to happen!).

我已经测试了很多方法,飞行模式,再次打开设备,唯一让它搞砸的是清除手机上某些Google应用程序中的数据(不太可能发生!)。

Please contribute to this if you have different experiences, my app is still in early testing stage.

如果您有不同的经历,请为此做出贡献,我的应用程序仍处于早期测试阶段。

#2


5  

I refactored ne0's answer into a static method, including the comments from snark.

我将ne0的答案重构为静态方法,包括来自snark的评论。

I call this method when my app starts - you'll need to enable your features at the TODO

我的应用程序启动时调用此方法 - 您需要在TODO启用您的功能

/**
 * This is how you check with Google if the user previously purchased a non-consumable IAP
 * @param context App Context
 */
public static void queryPlayStoreForPurchases(Context context)
{
    final IabHelper helper = new IabHelper(context, getPublicKey());

    helper.startSetup(new IabHelper.OnIabSetupFinishedListener() 
    {
         public void onIabSetupFinished(IabResult result) 
         {
               if (!result.isSuccess()) 
               {
                   Log.d("InApp", "In-app Billing setup failed: " + result);
               } 
               else 
               {  
                    helper.queryInventoryAsync(false, new IabHelper.QueryInventoryFinishedListener()
                    {
                        public void onQueryInventoryFinished(IabResult result, Inventory inventory)
                        {
                            // If the user has IAP'd the Pro version, let 'em have it.
                            if (inventory.hasPurchase(PRO_VERSION_SKU))
                            {
                                //TODO: ENABLE YOUR PRO FEATURES!! 

                                Log.d("IAP Check", "IAP Feature enabled!");
                            }
                            else
                            {
                                Log.d("IAP Check", "User has not purchased Pro version, not enabling features.");

                            }
                        }
                    });
               }
         }
    });
}

This will work across reboots and without a network connection, provided the user purchased the item.

如果用户购买了该项目,这将在重新启动和没有网络连接的情况下工作。

#3


3  

Since you already know that it's impossible to make it unhackable using this system, I would recommend not attempting to prevent hacking. What you are proposing is known as "Security through obscurity" and is usually a bad idea.

既然你已经知道使用这个系统不可能让它无法解决,我建议不要试图阻止黑客入侵。你提出的建议被称为“通过默默无闻的安全”,通常是一个坏主意。

My advice would be to try queryInventoryAsync() first, and only check your 'isPremium' flag if there is no internet connection.

我的建议是首先尝试queryInventoryAsync(),如果没有互联网连接,只检查你的'isPremium'标志。

There are also a few potential other ways of going about this, such as having separate free and premium apps, instead of an in app purchase. How other people handle this and the tools Google makes available might warrant an investigation.

还有一些其他可能的方法,例如拥有单独的免费和高级应用程序,而不是应用内购买。其他人如何处理此问题以及Google提供的工具可能需要进行调查。

queryInventoryAsync will automatically take into account uninstall and reinstalls, as it tracks purchases for the logged in user.

queryInventoryAsync会自动考虑卸载和重新安装,因为它会跟踪已登录用户的购买情况。

#1


16  

I too was making the same investigations, but during my testing I figured out that you do not need to store it, as Google do all the caching you need and I suspect (though I have not investigated it) that they are doing so as securely as possible (seeing as it in their interest too!)

我也在做同样的调查,但是在我的测试中我发现你不需要存储它,因为谷歌会做你需要的所有缓存,我怀疑(尽管我没有调查过)他们这样做是安全的尽可能(看到他们感兴趣的!)

So here is what i do

所以这就是我的所作所为

// Done in onCreate
mHelper = new IabHelper(this, getPublicKey());

mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
      if (!result.isSuccess()) {
         // Oh noes, there was a problem.
         Log("Problem setting up In-app Billing: " + result);
      } else {
         Log("onIabSetupFinished " + result.getResponse());
         mHelper.queryInventoryAsync(mGotInventoryListener);
     }
    }
});

// Called by button press
private void buyProUpgrade() {
    mHelper.launchPurchaseFlow(this, "android.test.purchased", 10001,   
           mPurchaseFinishedListener, ((TelephonyManager)this.getSystemService(Context.TELEPHONY_SERVICE)).getDeviceId());
}

// Get purchase response
private IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result, Purchase purchase) 
    {
       if (result.isFailure()) {
          Log("Error purchasing: " + result);
          return;
       }      
       else if (purchase.getSku().equals("android.test.purchased")) {
      Log("onIabPurchaseFinished GOT A RESPONSE.");
              mHelper.queryInventoryAsync(mGotInventoryListener);
      }
    }
};

// Get already purchased response
private IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
    public void onQueryInventoryFinished(IabResult result,
       Inventory inventory) {

       if (result.isFailure()) {
         // handle error here
           Log("Error checking inventory: " + result); 
       }
       else {
         // does the user have the premium upgrade?        
         mIsPremium = inventory.hasPurchase("android.test.purchased");        
         setTheme();

         Log("onQueryInventoryFinished GOT A RESPONSE (" + mIsPremium + ").");
       }
    }
};

So what happens here?

那么这里发生了什么?

The IAB is set up and calls startSetup, on a successful completion (as long as it has been run once with an internet connection and is set up correctly it will always succeed) we call queryInventoryAsync to find out what is already purchased (again if this has been called while online it always works while offline).

成功完成IAB设置并调用startSetup(只要它已通过Internet连接运行一次并且设置正确,它将始终成功)我们调用queryInventoryAsync来查找已购买的内容(如果这样的话)已经在线上调用它在离线时总能正常工作)。

So if a purchase is completed successfully (as can only be done while online) we call queryInventoryAsync to ensure that it has been called while online.

因此,如果购买成功完成(只能在线完成),我们调用queryInventoryAsync以确保在线时调用它。

Now there is no need to store anything to do with purchases and makes your app a lot less hackable.

现在没有必要存储任何与购买有关的东西,并且使你的应用程序不那么容易破解。

I have tested this many ways, flight mode, turning the devices off an on again and the only thing that messes it up is clearing data in some of the Google apps on the phone (Not likely to happen!).

我已经测试了很多方法,飞行模式,再次打开设备,唯一让它搞砸的是清除手机上某些Google应用程序中的数据(不太可能发生!)。

Please contribute to this if you have different experiences, my app is still in early testing stage.

如果您有不同的经历,请为此做出贡献,我的应用程序仍处于早期测试阶段。

#2


5  

I refactored ne0's answer into a static method, including the comments from snark.

我将ne0的答案重构为静态方法,包括来自snark的评论。

I call this method when my app starts - you'll need to enable your features at the TODO

我的应用程序启动时调用此方法 - 您需要在TODO启用您的功能

/**
 * This is how you check with Google if the user previously purchased a non-consumable IAP
 * @param context App Context
 */
public static void queryPlayStoreForPurchases(Context context)
{
    final IabHelper helper = new IabHelper(context, getPublicKey());

    helper.startSetup(new IabHelper.OnIabSetupFinishedListener() 
    {
         public void onIabSetupFinished(IabResult result) 
         {
               if (!result.isSuccess()) 
               {
                   Log.d("InApp", "In-app Billing setup failed: " + result);
               } 
               else 
               {  
                    helper.queryInventoryAsync(false, new IabHelper.QueryInventoryFinishedListener()
                    {
                        public void onQueryInventoryFinished(IabResult result, Inventory inventory)
                        {
                            // If the user has IAP'd the Pro version, let 'em have it.
                            if (inventory.hasPurchase(PRO_VERSION_SKU))
                            {
                                //TODO: ENABLE YOUR PRO FEATURES!! 

                                Log.d("IAP Check", "IAP Feature enabled!");
                            }
                            else
                            {
                                Log.d("IAP Check", "User has not purchased Pro version, not enabling features.");

                            }
                        }
                    });
               }
         }
    });
}

This will work across reboots and without a network connection, provided the user purchased the item.

如果用户购买了该项目,这将在重新启动和没有网络连接的情况下工作。

#3


3  

Since you already know that it's impossible to make it unhackable using this system, I would recommend not attempting to prevent hacking. What you are proposing is known as "Security through obscurity" and is usually a bad idea.

既然你已经知道使用这个系统不可能让它无法解决,我建议不要试图阻止黑客入侵。你提出的建议被称为“通过默默无闻的安全”,通常是一个坏主意。

My advice would be to try queryInventoryAsync() first, and only check your 'isPremium' flag if there is no internet connection.

我的建议是首先尝试queryInventoryAsync(),如果没有互联网连接,只检查你的'isPremium'标志。

There are also a few potential other ways of going about this, such as having separate free and premium apps, instead of an in app purchase. How other people handle this and the tools Google makes available might warrant an investigation.

还有一些其他可能的方法,例如拥有单独的免费和高级应用程序,而不是应用内购买。其他人如何处理此问题以及Google提供的工具可能需要进行调查。

queryInventoryAsync will automatically take into account uninstall and reinstalls, as it tracks purchases for the logged in user.

queryInventoryAsync会自动考虑卸载和重新安装,因为它会跟踪已登录用户的购买情况。