向您的应用添加客户端端许可证验证

警告:当您的应用在客户端执行许可证验证过程时,潜在攻击者更容易修改或删除与此验证过程关联的逻辑。

因此,我们强烈建议您改为执行服务器端许可证验证

设置发布者帐户和开发环境后(请参阅许可证设置),您就可以使用许可证验证库 (LVL) 向您的应用添加许可证验证。

使用 LVL 添加许可证验证涉及以下任务

  1. 向应用的清单中添加许可证权限
  2. 实现策略 — 您可以选择 LVL 中提供的完整实现之一,或创建自己的实现。
  3. 实现混淆器,如果您的Policy将缓存任何许可证响应数据。
  4. 在应用的主 Activity 中添加检查许可证的代码
  5. 实现设备限制器(可选,不推荐大多数应用使用)。

以下部分将描述这些任务。完成集成后,您应该能够成功编译您的应用,然后可以开始测试,如设置测试环境中所述。

有关 LVL 中包含的完整源文件集的概述,请参阅LVL 类和接口摘要

添加许可证权限

要使用 Google Play 应用将许可证检查发送到服务器,您的应用必须请求正确的权限com.android.vending.CHECK_LICENSE。如果您的应用未声明许可证权限但尝试启动许可证检查,则 LVL 会抛出安全异常。

要在您的应用中请求许可证权限,请将<uses-permission>元素声明为<manifest>的子元素,如下所示

<uses-permission android:name="com.android.vending.CHECK_LICENSE" />

例如,以下是 LVL 示例应用如何声明权限

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...">
    <!-- Devices >= 3 have version of Google Play that supports licensing. -->
    <uses-sdk android:minSdkVersion="3" />
    <!-- Required permission to check licensing. -->
    <uses-permission android:name="com.android.vending.CHECK_LICENSE" />
    ...
</manifest>

注意:目前,您无法在 LVL 库项目的清单中声明CHECK_LICENSE权限,因为 SDK 工具不会将其合并到依赖应用的清单中。相反,您必须在每个依赖应用的清单中声明该权限。

实现策略

Google Play 许可证服务本身不会确定是否应授予具有给定许可证的给定用户访问您的应用的权限。相反,该责任留给您在应用中提供的Policy实现。

Policy 是 LVL 声明的接口,旨在保存应用的逻辑,以根据许可证检查的结果允许或拒绝用户访问。要使用 LVL,您的应用必须提供Policy的实现。

The Policy接口声明了两种方法allowAccess()processServerResponse(),它们在处理来自许可证服务器的响应时由LicenseChecker实例调用。它还声明了一个名为LicenseResponse的枚举,该枚举指定了在对processServerResponse()的调用中传递的许可证响应值。

  • processServerResponse()允许您在确定是否授予访问权限之前,预处理从许可证服务器接收到的原始响应数据。

    典型的实现将从许可证响应中提取某些或所有字段,并将数据本地存储到持久性存储中,例如通过SharedPreferences存储,以确保数据可在应用调用和设备电源循环之间访问。例如,Policy将维护上次成功许可证检查的时间戳、重试次数、许可证有效期以及类似信息,而不是在每次启动应用时重置这些值。

    在本地存储响应数据时,Policy必须确保数据已进行混淆(请参阅以下实现混淆器)。

  • allowAccess() 根据可用的许可证响应数据(来自许可证服务器或缓存)或其他应用程序特定的信息来确定是否授予用户访问应用程序的权限。例如,allowAccess() 的实现可以考虑其他标准,例如从后端服务器检索到的使用情况或其他数据。在所有情况下,allowAccess() 的实现仅应在许可证服务器确定用户有权使用该应用程序,或者存在阻止许可证检查完成的瞬态网络或系统问题时才返回 true。在这种情况下,您的实现可以维护重试响应的计数,并在下一个许可证检查完成之前临时允许访问。

为了简化向应用程序添加许可证的过程并说明如何设计 Policy,LVL 包含两个完整的 Policy 实现,您可以直接使用或根据需要进行调整。

  • ServerManagedPolicy,一个灵活的 Policy,它使用服务器提供的设置和缓存的响应来管理各种网络条件下的访问;以及
  • StrictPolicy,它不缓存任何响应数据,并且仅当服务器返回许可证响应时才允许访问。

对于大多数应用程序,强烈建议使用 ServerManagedPolicy。ServerManagedPolicy 是 LVL 的默认策略,并与 LVL 示例应用程序集成。

自定义策略指南

在您的许可证实现中,您可以使用 LVL 提供的完整策略之一(ServerManagedPolicy 或 StrictPolicy),也可以创建自定义策略。对于任何类型的自定义策略,在您的实现中都需要了解并考虑几个重要的设计要点。

许可证服务器会应用通用请求限制,以防止过度使用资源导致拒绝服务。当应用程序超过请求限制时,许可证服务器会返回 503 响应,该响应会作为常规服务器错误传递到您的应用程序。这意味着在重置限制之前,用户将无法获得任何许可证响应,这可能会对用户造成无限期影响。

如果您正在设计自定义策略,我们建议 Policy

  1. 缓存(并正确混淆)最近成功的许可证响应到本地持久存储中。
  2. 对于所有许可证检查,只要缓存的响应有效,就返回缓存的响应,而不是向许可证服务器发出请求。强烈建议根据服务器提供的 VT 额外信息设置响应有效期。有关更多信息,请参阅 服务器响应额外信息
  3. 如果重试任何导致错误的请求,则使用指数退避周期。请注意,Google Play 客户端会自动重试失败的请求,因此在大多数情况下,您的 Policy 无需重试它们。
  4. 提供“宽限期”,允许用户在有限的时间或使用次数内访问您的应用程序,同时正在重试许可证检查。宽限期使用户能够在下一个许可证检查成功完成之前访问应用程序,并使您能够在没有有效的许可证响应时对访问应用程序进行硬限制。

根据上面列出的指南设计您的 Policy 至关重要,因为它可以确保为用户提供最佳体验,同时即使在错误情况下也能有效控制您的应用程序。

请注意,任何 Policy 都可以使用许可证服务器提供的设置来帮助管理有效期和缓存、重试宽限期等。提取服务器提供的设置非常简单,并且强烈建议使用它们。有关服务器设置的列表以及如何使用它们的信息,请参阅 服务器响应额外信息

ServerManagedPolicy

LVL 包含名为 ServerManagedPolicy 的 Policy 接口的完整且推荐的实现。该实现与 LVL 类集成,并用作库中的默认 Policy

ServerManagedPolicy 提供了许可证和重试响应的所有处理。它将所有响应数据本地缓存到 SharedPreferences 文件中,并使用应用程序的 Obfuscator 实现对其进行混淆。这确保了许可证响应数据是安全的,并且在设备电源循环之间持续存在。ServerManagedPolicy 提供了接口方法 processServerResponse()allowAccess() 的具体实现,还包括一组用于管理许可证响应的支持方法和类型。

重要的是,ServerManagedPolicy 的一个关键功能是使用服务器提供的设置作为在应用程序退款期和各种网络和错误条件下管理许可证的基础。当应用程序联系 Google Play 服务器进行许可证检查时,服务器会将几个设置作为键值对附加到某些许可证响应类型的额外字段中。例如,服务器会为应用程序的许可证有效期、重试宽限期和最大允许重试次数等提供推荐值。ServerManagedPolicy 在其 processServerResponse() 方法中从许可证响应中提取这些值,并在其 allowAccess() 方法中检查这些值。有关 ServerManagedPolicy 使用的服务器提供的设置的列表,请参阅 服务器响应额外信息

为了方便、获得最佳性能以及利用 Google Play 服务器的许可证设置的优势,**强烈建议使用 ServerManagedPolicy 作为您的许可证 Policy**。

如果您担心本地存储在 SharedPreferences 中的许可证响应数据的安全性,您可以使用更强大的混淆算法或设计更严格的 Policy,该策略不存储许可证数据。LVL 包含此类 Policy 的示例——有关更多信息,请参阅 StrictPolicy

要使用 ServerManagedPolicy,只需将其导入您的 Activity,创建实例,并在构造 LicenseChecker 时传递对该实例的引用。有关更多信息,请参阅 实例化 LicenseChecker 和 LicenseCheckerCallback

StrictPolicy

LVL 包含名为 StrictPolicy 的 Policy 接口的另一个完整实现。StrictPolicy 实现提供了比 ServerManagedPolicy 更严格的策略,因为它不允许用户访问应用程序,除非在访问时从服务器收到指示用户已获得许可的许可证响应。

StrictPolicy 的主要功能是它不会在本地持久存储中存储任何许可证响应数据。由于没有存储数据,因此不会跟踪重试请求,并且无法使用缓存的响应来满足许可证检查。该 Policy 仅在以下情况下允许访问:

  • 从许可证服务器收到许可证响应,并且
  • 许可证响应指示用户有权访问应用程序。

如果您的主要关注点是确保在所有可能的情况下,除非在使用时确认用户已获得许可,否则不允许任何用户访问应用程序,则使用 StrictPolicy 是合适的。此外,该策略提供了比 ServerManagedPolicy 略高的安全性——由于没有数据缓存在本地,因此恶意用户无法篡改缓存数据并获得对应用程序的访问权限。

同时,此 Policy 对普通用户提出了挑战,因为它意味着当没有可用的网络(蜂窝或 Wi-Fi)连接时,他们将无法访问应用程序。另一个副作用是您的应用程序将向服务器发送更多许可证检查请求,因为无法使用缓存的响应。

总的来说,此策略代表了在一定程度上牺牲用户便利性以换取绝对安全性并控制访问。在使用此 Policy 之前,请仔细考虑权衡。

要使用 StrictPolicy,只需将其导入您的 Activity,创建实例,并在构造 LicenseChecker 时传递对它的引用。有关更多信息,请参阅 实例化 LicenseChecker 和 LicenseCheckerCallback

典型的 Policy 实现需要将应用程序的许可证响应数据保存到持久存储中,以便在应用程序调用和设备电源循环之间可以访问。例如,Policy 将在持久存储中维护上次成功许可证检查的时间戳、重试次数、许可证有效期以及类似信息,而不是在每次启动应用程序时重置这些值。LVL 中包含的默认 Policy(ServerManagedPolicy)将许可证响应数据存储在 SharedPreferences 实例中,以确保数据持久化。

由于 Policy 将使用存储的许可证响应数据来确定是否允许或拒绝访问应用程序,因此它必须确保任何存储的数据都是安全的,并且设备上的 root 用户无法重用或操纵。具体来说,Policy 必须始终在存储之前使用对应用程序和设备唯一的密钥对数据进行混淆。使用既特定于应用程序又特定于设备的密钥进行混淆至关重要,因为它可以防止混淆后的数据在应用程序和设备之间共享。

LVL 帮助应用程序以安全、持久的方式存储其许可证响应数据。首先,它提供了一个 Obfuscator 接口,允许您的应用程序为存储的数据提供其选择的混淆算法。在此基础上,LVL 提供了辅助类 PreferenceObfuscator,它处理调用应用程序的 Obfuscator 类以及在 SharedPreferences 实例中读取和写入混淆数据的绝大部分工作。

LVL 提供了一个名为 AESObfuscator 的完整 Obfuscator 实现,它使用 AES 加密来混淆数据。您可以在应用程序中直接使用 AESObfuscator,也可以根据需要进行调整。如果您使用的是缓存许可证响应数据的 Policy(例如 ServerManagedPolicy),则强烈建议使用 AESObfuscator 作为 Obfuscator 实现的基础。有关更多信息,请参阅下一节。

AESObfuscator

LVL 包含了 Obfuscator 接口的一个完整且推荐的实现,名为 AESObfuscator。此实现已集成到 LVL 示例应用程序中,并用作库中的默认 Obfuscator

AESObfuscator 通过使用 AES 对数据进行加密和解密来提供安全的数据混淆,这些数据在写入或读取存储时都会进行处理。 Obfuscator 使用应用程序提供的三个数据字段来为加密设置种子。

  1. 盐 - 用于每次(取消)混淆操作的一组随机字节。
  2. 应用程序标识符字符串,通常是应用程序的包名。
  3. 设备标识符字符串,尽可能地从多个设备特定来源派生而来,以使其尽可能唯一。

要使用 AESObfuscator,首先将其导入到您的 Activity 中。声明一个私有的静态 final 数组来保存盐字节,并将其初始化为 20 个随机生成的字节。

Kotlin

// Generate 20 random bytes, and put them here.
private val SALT = byteArrayOf(
        -46, 65, 30, -128, -103, -57, 74, -64, 51, 88,
        -95, -45, 77, -117, -36, -113, -11, 32, -64, 89
)

Java

...
    // Generate 20 random bytes, and put them here.
    private static final byte[] SALT = new byte[] {
     -46, 65, 30, -128, -103, -57, 74, -64, 51, 88, -95,
     -45, 77, -117, -36, -113, -11, 32, -64, 89
     };
    ...

接下来,声明一个变量来保存设备标识符,并以任何所需的方式为其生成一个值。例如,LVL 中包含的示例应用程序查询系统设置以获取 android.Settings.Secure.ANDROID_ID,该 ID 对每个设备都是唯一的。

请注意,根据您使用的 API,您的应用程序可能需要请求其他权限才能获取设备特定信息。例如,要查询 TelephonyManager 以获取设备 IMEI 或相关数据,应用程序还需要在其清单中请求 android.permission.READ_PHONE_STATE 权限。

在出于唯一获取设备特定信息以在您的 Obfuscator 中使用而请求新权限之前,请考虑这样做可能会如何影响您的应用程序或其在 Google Play 上的过滤(因为某些权限会导致 SDK 构建工具添加关联的 <uses-feature>)。

最后,构造 AESObfuscator 的实例,并传递盐、应用程序标识符和设备标识符。您可以在构造 PolicyLicenseChecker 时直接构造实例。例如

Kotlin

    ...
    // Construct the LicenseChecker with a Policy.
    private val checker = LicenseChecker(
            this,
            ServerManagedPolicy(this, AESObfuscator(SALT, packageName, deviceId)),
            BASE64_PUBLIC_KEY
    )
    ...

Java

    ...
    // Construct the LicenseChecker with a Policy.
    checker = new LicenseChecker(
        this, new ServerManagedPolicy(this,
            new AESObfuscator(SALT, getPackageName(), deviceId)),
        BASE64_PUBLIC_KEY // Your public licensing key.
        );
    ...

有关完整示例,请参阅 LVL 示例应用程序中的 MainActivity。

从 Activity 检查许可证

一旦您为管理对应用程序的访问权限实现了 Policy,下一步就是在您的应用程序中添加许可证检查,该检查会在需要时启动对许可证服务器的查询,并根据许可证响应管理对应用程序的访问。添加许可证检查和处理响应的所有工作都在您的主 Activity 源文件中进行。

要添加许可证检查并处理响应,您必须

  1. 添加导入
  2. 实现 LicenseCheckerCallback 作为私有内部类
  3. 创建 Handler 用于将 LicenseCheckerCallback 发布到 UI 线程
  4. 实例化 LicenseChecker 和 LicenseCheckerCallback
  5. 调用 checkAccess() 以启动许可证检查
  6. 嵌入您的用于许可证的公钥
  7. 调用您的 LicenseChecker 的 onDestroy() 方法 以关闭 IPC 连接。

以下各节描述了这些任务。

许可证检查和响应概述

在大多数情况下,您应该将许可证检查添加到应用程序的主 Activity 中的 onCreate() 方法中。这可以确保当用户直接启动您的应用程序时,许可证检查会立即被调用。在某些情况下,您也可以在其他位置添加许可证检查。例如,如果您的应用程序包含多个 Activity 组件,其他应用程序可以通过 Intent 启动这些组件,则可以在这些 Activity 中添加许可证检查。

许可证检查包含两个主要操作

  • 调用一个方法来启动许可证检查 - 在 LVL 中,这是对您构造的 LicenseChecker 对象的 checkAccess() 方法的调用。
  • 返回许可证检查结果的回调。在 LVL 中,这是一个您实现的 LicenseCheckerCallback 接口。该接口声明了两个方法,allow()dontAllow(),它们由库根据许可证检查的结果调用。您使用所需的任何逻辑来实现这两个方法,以允许或拒绝用户访问您的应用程序。请注意,这些方法不决定是否允许访问 - 此决定是您的 Policy 实现的责任。相反,这些方法只是为如何允许和拒绝访问(以及处理应用程序错误)提供了应用程序行为。

    allow()dontAllow() 方法确实为其响应提供了“原因”,该原因可以是 Policy 值之一,LICENSEDNOT_LICENSEDRETRY。特别是,您应该处理方法接收 dontAllow()RETRY 响应的情况,并为用户提供一个“重试”按钮,这可能是因为请求期间服务不可用。

图 1. 典型许可证检查交互概述。

上图说明了典型许可证检查是如何进行的

  1. 应用程序主 Activity 中的代码实例化 LicenseCheckerCallbackLicenseChecker 对象。在构造 LicenseChecker 时,代码将 Context、要使用的 Policy 实现以及发布者帐户的用于许可证的公钥作为参数传递。
  2. 然后,代码在 LicenseChecker 对象上调用 checkAccess() 方法。该方法实现调用 Policy 以确定是否在本地缓存了有效的许可证响应,在 SharedPreferences 中。
    • 如果是,则 checkAccess() 实现将调用 allow()
    • 否则,LicenseChecker 将启动许可证检查请求,该请求将发送到许可证服务器。

    注意:当您执行草稿应用程序的许可证检查时,许可证服务器始终返回 LICENSED

  3. 收到响应后,LicenseChecker 将创建一个 LicenseValidator,该验证器将验证签名的许可证数据并提取响应的字段,然后将其传递给您的 Policy 以进行进一步评估。
    • 如果许可证有效,则 Policy 将在 SharedPreferences 中缓存响应,并通知验证器,然后验证器将在 LicenseCheckerCallback 对象上调用 allow() 方法。
    • 如果许可证无效,则 Policy 将通知验证器,验证器将在 LicenseCheckerCallback 上调用 dontAllow() 方法。
  4. 在发生可恢复的本地或服务器错误的情况下,例如网络不可用以发送请求时,LicenseCheckerRETRY 响应传递到您的 Policy 对象的 processServerResponse() 方法。

    此外,allow()dontAllow() 回调方法都接收一个 reason 参数。 allow() 方法的原因通常是 Policy.LICENSEDPolicy.RETRY,而 dontAllow() 的原因通常是 Policy.NOT_LICENSEDPolicy.RETRY。这些响应值非常有用,因此您可以向用户显示适当的响应,例如,当 dontAllow()Policy.RETRY 响应时提供“重试”按钮,这可能是因为服务不可用。

  5. 在发生应用程序错误的情况下,例如当应用程序尝试检查无效包名的许可证时,LicenseChecker 将错误响应传递到 LicenseCheckerCallback 的 applicationError() 方法。

请注意,除了启动许可证检查和处理结果(在以下各节中进行了描述)之外,您的应用程序还需要提供 Policy 实现,如果 Policy 存储响应数据(例如 ServerManagedPolicy),则还需要提供 Obfuscator 实现

添加导入

首先,打开应用程序主 Activity 的类文件并从 LVL 包中导入 LicenseCheckerLicenseCheckerCallback

Kotlin

import com.google.android.vending.licensing.LicenseChecker
import com.google.android.vending.licensing.LicenseCheckerCallback

Java

import com.google.android.vending.licensing.LicenseChecker;
import com.google.android.vending.licensing.LicenseCheckerCallback;

如果您使用的是 LVL 提供的默认 Policy 实现 ServerManagedPolicy,也请将其导入,以及 AESObfuscator。如果您使用的是自定义 PolicyObfuscator,请导入这些内容。

Kotlin

import com.google.android.vending.licensing.ServerManagedPolicy
import com.google.android.vending.licensing.AESObfuscator

Java

import com.google.android.vending.licensing.ServerManagedPolicy;
import com.google.android.vending.licensing.AESObfuscator;

实现 LicenseCheckerCallback 作为私有内部类

LicenseCheckerCallback 是 LVL 提供的用于处理许可证检查结果的接口。要支持使用 LVL 进行许可证管理,您必须实现 LicenseCheckerCallback 及其方法以允许或拒绝对应用程序的访问。

许可证检查的结果始终是调用其中一个LicenseCheckerCallback方法,该方法基于对响应有效负载、服务器响应代码本身以及您的Policy提供的任何额外处理的验证进行调用。您的应用程序可以以任何需要的方式实现这些方法。通常,最好使这些方法保持简单,将其限制在管理 UI 状态和应用程序访问权限。如果您想添加对许可证响应的进一步处理,例如通过联系后端服务器或应用自定义约束,则应考虑将该代码合并到您的Policy中,而不是将其放入LicenseCheckerCallback方法中。

在大多数情况下,您应该在应用程序的主 Activity 类中将LicenseCheckerCallback的实现声明为一个私有类。

根据需要实现allow()dontAllow()方法。首先,您可以在这些方法中使用简单的结果处理行为,例如在对话框中显示许可证结果。这有助于您更快地运行应用程序,并有助于调试。稍后,在确定所需的确切行为后,您可以添加更复杂的处理。

dontAllow()中处理未授权响应的一些建议包括

  • 如果提供的reasonPolicy.RETRY,则向用户显示“重试”对话框,其中包括一个按钮以启动新的许可证检查。
  • 显示“购买此应用程序”对话框,其中包括一个按钮,该按钮将用户深度链接到 Google Play 上的应用程序详细信息页面,用户可以在该页面上购买该应用程序。有关如何设置此类链接的更多信息,请参阅链接到您的产品
  • 显示一个 Toast 通知,指示应用程序的功能有限,因为它未获得许可。

以下示例显示了 LVL 示例应用程序如何实现LicenseCheckerCallback,以及在对话框中显示许可证检查结果的方法。

Kotlin

private inner class MyLicenseCheckerCallback : LicenseCheckerCallback {

    override fun allow(reason: Int) {
        if (isFinishing) {
            // Don't update UI if Activity is finishing.
            return
        }
        // Should allow user access.
        displayResult(getString(R.string.allow))
    }

    override fun dontAllow(reason: Int) {
        if (isFinishing) {
            // Don't update UI if Activity is finishing.
            return
        }
        displayResult(getString(R.string.dont_allow))

        if (reason == Policy.RETRY) {
            // If the reason received from the policy is RETRY, it was probably
            // due to a loss of connection with the service, so we should give the
            // user a chance to retry. So show a dialog to retry.
            showDialog(DIALOG_RETRY)
        } else {
            // Otherwise, the user isn't licensed to use this app.
            // Your response should always inform the user that the application
            // isn't licensed, but your behavior at that point can vary. You might
            // provide the user a limited access version of your app or you can
            // take them to Google Play to purchase the app.
            showDialog(DIALOG_GOTOMARKET)
        }
    }
}

Java

private class MyLicenseCheckerCallback implements LicenseCheckerCallback {
    public void allow(int reason) {
        if (isFinishing()) {
            // Don't update UI if Activity is finishing.
            return;
        }
        // Should allow user access.
        displayResult(getString(R.string.allow));
    }

    public void dontAllow(int reason) {
        if (isFinishing()) {
            // Don't update UI if Activity is finishing.
            return;
        }
        displayResult(getString(R.string.dont_allow));

        if (reason == Policy.RETRY) {
            // If the reason received from the policy is RETRY, it was probably
            // due to a loss of connection with the service, so we should give the
            // user a chance to retry. So show a dialog to retry.
            showDialog(DIALOG_RETRY);
        } else {
            // Otherwise, the user isn't licensed to use this app.
            // Your response should always inform the user that the application
            // isn't licensed, but your behavior at that point can vary. You might
            // provide the user a limited access version of your app or you can
            // take them to Google Play to purchase the app.
            showDialog(DIALOG_GOTOMARKET);
        }
    }
}

此外,您还应该实现applicationError()方法,LVL 通过该方法调用您的应用程序以处理不可重试的错误。有关此类错误的列表,请参阅服务器响应代码(位于许可证参考中)。您可以以任何需要的方式实现该方法。在大多数情况下,该方法应记录错误代码并调用dontAllow()

创建一个 Handler 用于将 LicenseCheckerCallback 发布到 UI 线程

在许可证检查期间,LVL 将请求传递给 Google Play 应用程序,后者处理与许可证服务器的通信。LVL 通过异步 IPC(使用Binder)传递请求,因此实际的处理和网络通信不会在您的应用程序管理的线程上进行。类似地,当 Google Play 应用程序收到结果时,它会通过 IPC 调用回调方法,该方法依次在应用程序进程中的 IPC 线程池中执行。

LicenseChecker类管理您的应用程序与 Google Play 应用程序的 IPC 通信,包括发送请求的调用和接收响应的回调。LicenseChecker还跟踪打开的许可证请求并管理其超时。

以便能够正确处理超时并处理传入的响应而不影响应用程序的 UI 线程,LicenseChecker在实例化时会生成一个后台线程。在该线程中,它执行所有许可证检查结果的处理,无论结果是从服务器接收到的响应还是超时错误。在处理结束时,LVL 从后台线程调用您的LicenseCheckerCallback方法。

对于您的应用程序,这意味着

  1. 您的LicenseCheckerCallback方法将在许多情况下从后台线程调用。
  2. 除非您在 UI 线程中创建了一个 Handler 并让您的回调方法发布到该 Handler,否则这些方法将无法更新状态或在 UI 线程中调用任何处理。

如果您希望您的LicenseCheckerCallback方法更新 UI 线程,请在主 Activity 的onCreate()方法中实例化一个Handler,如下所示。在此示例中,LVL 示例应用程序的LicenseCheckerCallback方法(请参见上文)调用displayResult()以通过 Handler 的post()方法更新 UI 线程。

Kotlin

    private lateinit var handler: Handler

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        handler = Handler()
    }

Java

    private Handler handler;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        handler = new Handler();
    }

然后,在您的LicenseCheckerCallback方法中,您可以使用 Handler 方法将 Runnable 或 Message 对象发布到 Handler。以下是 LVL 中包含的示例应用程序如何将 Runnable 发布到 UI 线程中的 Handler 以显示许可证状态。

Kotlin

private fun displayResult(result: String) {
    handler.post {
        statusText.text = result
        setProgressBarIndeterminateVisibility(false)
        checkLicenseButton.isEnabled = true
    }
}

Java

private void displayResult(final String result) {
        handler.post(new Runnable() {
            public void run() {
                statusText.setText(result);
                setProgressBarIndeterminateVisibility(false);
                checkLicenseButton.setEnabled(true);
            }
        });
    }

实例化 LicenseChecker 和 LicenseCheckerCallback

在主 Activity 的onCreate()方法中,创建 LicenseCheckerCallback 和LicenseChecker的私有实例。您必须先实例化LicenseCheckerCallback,因为在调用LicenseChecker的构造函数时,需要将对该实例的引用传递给它。

实例化LicenseChecker时,需要传入以下参数

  • 应用程序Context
  • 对用于许可证检查的Policy实现的引用。在大多数情况下,您将使用 LVL 提供的默认Policy实现 ServerManagedPolicy。
  • 保存发布者帐户的许可证公钥的 String 变量。

如果您使用的是 ServerManagedPolicy,则无需直接访问该类,因此可以在LicenseChecker构造函数中实例化它,如下面的示例所示。请注意,在构造 ServerManagedPolicy 时,需要将对新 Obfuscator 实例的引用传递给它。

以下示例显示了从 Activity 类的onCreate()方法实例化LicenseCheckerLicenseCheckerCallback

Kotlin

class MainActivity : AppCompatActivity() {
    ...
    private lateinit var licenseCheckerCallback: LicenseCheckerCallback
    private lateinit var checker: LicenseChecker

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Construct the LicenseCheckerCallback. The library calls this when done.
        licenseCheckerCallback = MyLicenseCheckerCallback()

        // Construct the LicenseChecker with a Policy.
        checker = LicenseChecker(
                this,
                ServerManagedPolicy(this, AESObfuscator(SALT, packageName, deviceId)),
                BASE64_PUBLIC_KEY // Your public licensing key.
        )
        ...
    }
}

Java

public class MainActivity extends Activity {
    ...
    private LicenseCheckerCallback licenseCheckerCallback;
    private LicenseChecker checker;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Construct the LicenseCheckerCallback. The library calls this when done.
        licenseCheckerCallback = new MyLicenseCheckerCallback();

        // Construct the LicenseChecker with a Policy.
        checker = new LicenseChecker(
            this, new ServerManagedPolicy(this,
                new AESObfuscator(SALT, getPackageName(), deviceId)),
            BASE64_PUBLIC_KEY // Your public licensing key.
            );
        ...
    }
}

请注意,LicenseChecker仅在本地缓存有有效的许可证响应时才从 UI 线程调用LicenseCheckerCallback方法。如果许可证检查发送到服务器,则回调始终源自后台线程,即使对于网络错误也是如此。

调用 checkAccess() 以启动许可证检查

在您的主 Activity 中,添加对LicenseChecker实例的checkAccess()方法的调用。在调用中,将对您的LicenseCheckerCallback实例的引用作为参数传递。如果您需要在调用之前处理任何特殊的 UI 效果或状态管理,您可能会发现从包装器方法调用checkAccess()很有用。例如,LVL 示例应用程序从doCheck()包装器方法调用checkAccess()

Kotlin

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        // Call a wrapper method that initiates the license check
        doCheck()
        ...
    }
    ...
    private fun doCheck() {
        checkLicenseButton.isEnabled = false
        setProgressBarIndeterminateVisibility(true)
        statusText.setText(R.string.checking_license)
        checker.checkAccess(licenseCheckerCallback)
    }

Java

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Call a wrapper method that initiates the license check
        doCheck();
        ...
    }
    ...
    private void doCheck() {
        checkLicenseButton.setEnabled(false);
        setProgressBarIndeterminateVisibility(true);
        statusText.setText(R.string.checking_license);
        checker.checkAccess(licenseCheckerCallback);
    }

嵌入您的许可证公钥

对于每个应用程序,Google Play 服务会自动生成一个用于许可和应用内结算的 2048 位 RSA 公钥/私钥对。密钥对与应用程序唯一关联。尽管与应用程序关联,但密钥对**与**用于签署应用程序的密钥(或从中派生的密钥)**不同**。

Google Play Console 会向登录 Play Console 的任何开发人员公开许可证公钥,但会将私钥安全地保存在所有用户都无法访问的位置。当应用程序请求发布到您帐户中的应用程序的许可证检查时,许可证服务器会使用应用程序密钥对的私钥对许可证响应进行签名。当 LVL 收到响应时,它会使用应用程序提供的公钥验证许可证响应的签名。

要向应用程序添加许可,您必须获取应用程序的许可证公钥并将其复制到应用程序中。以下是如何查找应用程序的许可证公钥

  1. 转到 Google Play Console 并登录。确保您登录到发布(或将发布)您要许可的应用程序的帐户。
  2. 在应用程序详细信息页面中,找到**服务和 API**链接并单击它。
  3. 在**服务和 API**页面中,找到**许可和应用内结算**部分。您的许可证公钥在**此应用程序的许可证密钥**字段中给出。

要将公钥添加到应用程序,只需将密钥字符串从该字段复制/粘贴到应用程序中,作为 String 变量BASE64_PUBLIC_KEY的值。复制时,请确保已选择整个密钥字符串,而不要省略任何字符。

以下是 LVL 示例应用程序中的一个示例

Kotlin

private const val BASE64_PUBLIC_KEY = "MIIBIjANBgkqhkiG ... " //truncated for this example
class LicensingActivity : AppCompatActivity() {
    ...
}

Java

public class MainActivity extends Activity {
    private static final String BASE64_PUBLIC_KEY = "MIIBIjANBgkqhkiG ... "; //truncated for this example
    ...
}

调用 LicenseChecker 的 onDestroy() 方法以关闭 IPC 连接

最后,为了让 LVL 在应用程序Context更改之前进行清理,请从 Activity 的onDestroy()实现中添加对LicenseCheckeronDestroy()方法的调用。该调用会导致LicenseChecker正确关闭与 Google Play 应用程序的 ILicensingService 的任何打开的 IPC 连接,并删除对该服务和处理程序的任何本地引用。

未能调用LicenseCheckeronDestroy()方法会导致应用程序生命周期出现问题。例如,如果用户在许可证检查处于活动状态时更改屏幕方向,则应用程序Context将被销毁。如果您的应用程序没有正确关闭LicenseChecker的 IPC 连接,则在收到响应时,应用程序将崩溃。类似地,如果用户在许可证检查正在进行时退出应用程序,则在收到响应时,应用程序将崩溃,除非它已正确调用LicenseCheckeronDestroy()方法以断开与服务的连接。

以下是 LVL 中包含的示例应用程序中的一个示例,其中mCheckerLicenseChecker实例

Kotlin

    override fun onDestroy() {
        super.onDestroy()
        checker.onDestroy()
        ...
    }

Java

    @Override
    protected void onDestroy() {
        super.onDestroy();
        checker.onDestroy();
        ...
    }

如果您正在扩展或修改LicenseChecker,您可能还需要调用LicenseCheckerfinishCheck()方法,以清理任何打开的 IPC 连接。

实现 DeviceLimiter

在某些情况下,您可能希望您的 Policy 限制允许使用单个许可证的实际设备数量。这可以防止用户将已许可的应用程序移动到多个设备上,并在同一帐户 ID 下使用这些设备上的应用程序。它还可以防止用户通过向其他个人提供与许可证关联的帐户信息来“共享”应用程序,然后这些个人可以在其设备上登录该帐户并访问应用程序的许可证。

LVL 通过提供 DeviceLimiter 接口支持每个设备的许可,该接口声明了一个方法 allowDeviceAccess()。当 LicenseValidator 处理来自许可服务器的响应时,它会调用 allowDeviceAccess(),并传递从响应中提取的用户 ID 字符串。

如果您不想支持设备限制,则 **无需执行任何操作** - LicenseChecker 类会自动使用名为 NullDeviceLimiter 的默认实现。顾名思义,NullDeviceLimiter 是一个“无操作”类,其 allowDeviceAccess() 方法仅对所有用户和设备返回 LICENSED 响应。

注意:对于大多数应用程序,**不建议**使用每个设备的许可,因为

  • 它要求您提供一个后端服务器来管理用户和设备的映射,以及
  • 它可能会意外导致用户被拒绝访问他们已在其他设备上合法购买的应用程序。

混淆您的代码

为了确保应用程序的安全性,特别是对于使用许可和/或自定义约束和保护的付费应用程序,混淆应用程序代码非常重要。正确混淆代码可以使恶意用户更难以反编译应用程序的字节码、修改它(例如删除许可证检查),然后重新编译它。

有几个混淆器程序可用于 Android 应用程序,包括 ProGuard,它还提供代码优化功能。强烈建议所有使用 Google Play 许可的应用程序使用 ProGuard 或类似程序来混淆其代码。

发布已许可的应用程序

完成许可证实施测试后,您就可以在 Google Play 上发布应用程序了。按照正常的步骤 准备签名,然后 发布应用程序

获取支持的地方

如果您在实现或部署应用程序中的发布时有任何疑问或遇到问题,请使用下表中列出的支持资源。通过将您的查询发送到正确的论坛,您可以更快地获得所需的帮助。

表 2. Google Play 许可服务的开发者支持资源。

支持类型 资源 主题范围
开发和测试问题 Google 论坛:android-developers LVL 下载和集成、库项目、Policy 问题、用户体验想法、响应处理、Obfuscator、IPC、测试环境设置
Stack Overflow:http://stackoverflow.com/questions/tagged/android
帐户、发布和部署问题 Google Play 帮助论坛 发布者帐户、许可证密钥对、测试帐户、服务器响应、测试响应、应用程序部署和结果
市场许可支持常见问题解答
LVL 问题跟踪器 Marketlicensing 项目问题跟踪器 与 LVL 源代码类和接口实现相关的错误和问题报告

有关如何发布到上述组的常规信息,请参阅开发者支持资源页面上的 社区资源 部分。

其他资源

LVL 附带的示例应用程序提供了关于如何在 MainActivity 类中启动许可证检查和处理结果的完整示例。