为应用添加客户端许可验证

警告:当您的应用在客户端执行许可验证流程时,潜在的攻击者可以更轻松地修改或移除与此验证流程相关联的逻辑。

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

设置发布商账号和开发环境(请参阅设置许可)后,即可使用许可验证库 (LVL) 为应用添加许可验证。

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

  1. 向您的应用清单中添加许可权限
  2. 实现政策 - 您可以选择 LVL 中提供的任一完整实现,或创建自己的实现。
  3. 如果 Policy 将缓存任何许可响应数据,请实现混淆器
  4. 在应用的主 activity 中添加代码以检查许可
  5. 实现 DeviceLimiter(可选,对于大多数应用,不建议这样做。)。

下面几部分介绍了这些任务。完成集成后,您应该能够成功编译应用并开始测试,如设置测试环境中所述。

如需简要了解 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 实现确定。

政策是由 LVL 声明的接口,用于根据许可检查结果保留应用的逻辑,以允许或禁止用户访问。如需使用 LVL,应用必须提供 Policy 实现。

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 额外项设置响应有效性。如需了解详情,请参阅服务器响应 Extras
  3. 如果重试任何请求导致出现错误,则使用指数退避期。请注意,Google Play 客户端会自动重试失败的请求,因此在大多数情况下,您的 Policy 无需重试这些请求。
  4. 提供“宽限期”,在重试许可检查的同时,允许用户在限定时间内访问应用或限定用户访问应用的次数。宽限期对用户的好处在于:允许用户访问应用,直到成功完成下一次许可检查;宽限期对您的好处在于:在没有有效许可响应的情况下,对应用的访问施加硬性限制。

请务必根据上文列出的准则设计 Policy,因为这样可以确保尽可能向用户提供最佳体验,同时即使在出现错误的情况下您也可有效控制应用。

请注意,任何 Policy 都可以使用许可服务器提供的设置帮助管理有效性和缓存、重试宽限期等。提取服务器提供的设置非常简单,强烈建议您充分利用这些设置。有关如何提取和使用 extras 的示例,请参阅 ServerManagedPolicy 实现。有关服务器设置列表以及如何使用这些设置的信息,请参阅服务器响应 Extras

ServerManagedPolicy

LVL 包含名为 ServerManagedPolicy 的 Policy 接口的完整实现(建议使用此实现)。该实现已与 LVL 类集成,并用作库中的默认 Policy

ServerManagedPolicy 提供了许可和重试响应的整套处理流程。它将所有响应数据在本地缓存在 SharedPreferences 文件中,并通过应用的 Obfuscator 实现对其进行混淆。这可以确保许可响应数据安全无虞,并且可在设备重启后持久保存。ServerManagedPolicy 提供接口方法 processServerResponse()allowAccess() 的具体实现,并且还包括一组用于管理许可响应的支持方法和类型。

必须注意的是,ServerManagedPolicy 的一项关键功能是使用服务器提供的设置作为基础,在应用退款期间通过不同的网络和错误条件管理许可。当应用与 Google Play 服务器联系以进行许可检查时,服务器会在某些许可响应类型的 extras 字段中附加多个设置作为键值对。例如,服务器为应用的许可有效期、重试宽限期和允许的最大重试次数等提供建议值。ServerManagedPolicy 使用 processServerResponse() 方法从许可响应中提取值,并使用 allowAccess() 方法对其进行检查。有关 ServerManagedPolicy 使用的服务器提供的设置列表,请参阅服务器响应 Extras

为了方便起见,获得最佳性能,并且享受使用 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 将使用存储的许可响应数据确定是允许还是禁止应用访问,因此必须确保所有存储数据都是安全的,并且不能由设备上的根用户重复使用或操纵。具体来说,Policy 必须使用应用和设备专用的密钥,在存储数据前对其进行混淆。务必使用应用专用和设备专用的密钥进行混淆,因为这可以防止在应用和设备之间共享混淆后的数据。

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

LVL 提供名为 AESOffuscator 的 Obfuscator 的完整实现,其使用 AES 加密混淆数据。您可以在应用中使用 AESObfuscator,而无需进行修改,也可以根据需要进行调整。如果您使用的是缓存许可响应数据的 Policy(例如 ServerManagedPolicy),则强烈建议您使用 AESObfuscator 作为 Obfuscator 的实现基础。如需了解详情,请参阅下一部分。

AESObfuscator

LVL 包含名为 AESObfuscator 的 Obfuscator 接口的推荐完整实现。该实现与 LVL 示例应用集成,并用作库中的默认 Obfuscator

AESObfuscator 使用 AES 在数据写入存储区或从存储区中读取数据时进行加密和解密,从而实现数据的安全混淆。Obfuscator 使用应用提供的以下三个数据字段生成加密种子:

  1. 盐 - 用于每次(反)混淆的随机字节数组。
  2. 应用标识符字符串,通常是应用的软件包名称。
  3. 设备标识符字符串,从尽可能多的设备专用来源派生,以使其具有唯一性。

如需使用 AESObfuscator,请先将其导入 Activity。声明一个专用的静态最终数组以保存盐字节并将其初始化为 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 的系统设置,这对于每个设备而言都是唯一的。

请注意,根据您使用的 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. 创建处理程序,以从 LicenseCheckerCallback 发布到界面线程
  4. 实例化 LicenseChecker 和 LicenseCheckerCallback
  5. 调用 checkAccess() 以启动许可检查
  6. 嵌入许可公钥
  7. 调用 LicenseChecker 的 onDestroy() 方法以关闭 IPC 连接。

下面几部分介绍了这些任务。

许可检查和响应概述

在大多数情况下,应使用 onCreate() 方法将许可检查添加到应用的主 Activity 中。这样可以确保用户直接启动应用时,系统会立即调用许可检查。在某些情况下,您还可以在其他位置添加许可检查。例如,如果应用包含其他应用可通过 Intent 启动的多个 Activity 组件,就可以在这些 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 会创建一个 LicenseValaidator,以验证已签名许可数据并提取响应字段,然后将它们传递到 Policy 以进行进一步评估。
    • 如果许可有效,Policy 会将响应缓存在 SharedPreferences 中,然后通知验证器,再由验证器调用 LicenseCheckerCallback 对象的 allow() 方法。
    • 如果许可无效,Policy 会通知验证器,再由验证器调用 LicenseCheckerCallbackdontAllow() 方法。
  4. 如果发生可恢复的本地或服务器错误,例如网络无法发送请求时,LicenseChecker 会将 RETRY 响应传递至 Policy 对象的 processServerResponse() 方法。

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

  5. 如果发生应用错误,例如当应用尝试检查无效软件包名称的许可时,LicenseChecker 会将错误响应传递至 LicenseCheckerCallback 的 applicationError() 方法。

请注意,除以下部分所述的启动许可检查和处理结果外,应用还需要提供政策实现,如果用 Policy 存储响应数据(例如 ServerManagedPolicy),还需要提供 混淆器实现。

添加导入

首先,打开应用主 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 提供的任何其他处理。应用可以通过任何所需的方式实现这些方法。通常,最好确保方法简单易用,使其仅限用于管理界面状态和应用访问。如果您希望进一步处理许可响应,例如联系后端服务器或应用自定义限制,则应考虑将此代码合并到 Policy 中,而不是将其放入 LicenseCheckerCallback 方法中。

在大多数情况下,应在应用的主 activity 类中将 LicenseCheckerCallback 的实现声明为私有类。

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

有关处理 dontAllow() 中未经许可的响应的一些建议包括:

  • 向用户显示“重试”对话框,其中包括一个按钮,用于在所提供的 reasonPolicy.RETRY 时启动新的许可检查。
  • 显示“购买此应用”对话框,其中包括将用户深入链接到 Google Play 应用详情页面的按钮,用户可以在此页面中购买应用。如需详细了解如何设置此类链接,请参阅链接到产品
  • 显示消息框通知,指示应用的功能受到限制,因为它未经许可。

以下示例展示了 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()

创建处理程序,以从 LicenseCheckerCallback 发布到界面线程

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

LicenseChecker 类管理应用与 Google Play 应用的 IPC 通信,包括发送请求的调用以及接收响应的回调。LicenseChecker 还会跟踪未解决的许可请求并管理其超时。

为了能够正确处理超时以及传入的响应而不影响应用的界面线程,LicenseChecker 会在实例化时生成后台线程。此线程会处理所有许可检查结果,无论结果是从服务器收到的响应还是超时错误。处理结束时,LVL 会从后台线程调用 LicenseCheckerCallback 方法。

对于应用,这意味着:

  1. 在很多情况下,系统会从后台线程调用 LicenseCheckerCallback 方法。
  2. 除非在界面线程中创建处理程序并将回调方法发布到处理程序,否则这些方法将无法更新状态或调用界面线程中的任何处理。

如果要使用 LicenseCheckerCallback 方法更新界面线程,请使用主 activity 的 onCreate() 方法实例化 Handler,如下所示。在此示例中,LVL 示例应用的 LicenseCheckerCallback 方法(参见上文)调用 displayResult(),以通过处理程序的 post() 方法更新界面线程。

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 方法,可以使用处理程序方法将 Runnable 或 Message 对象发布到处理程序。下文介绍了 LVL 中包含的示例应用如何在界面线程中将 Runnable 发布到处理程序以显示许可状态。

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。
  • 包含发布商账号的许可公钥的字符串变量。

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

以下示例显示了如何使用 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 才会从界面线程调用 LicenseCheckerCallback 方法。如果将许可检查发送到服务器,回调会始终来自后台线程,即使出现网络错误也是如此。

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

在主 Activity 中,添加对 LicenseChecker 实例的 checkAccess() 方法的调用。在调用中,将对 LicenseCheckerCallback 实例的引用作为参数传递。如果需要在调用之前处理任何特殊界面效果或状态管理,您可能会发现使用封装容器方法调用 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 管理中心会向所有登录 Play 管理中心的开发者公开许可公钥,但会将私钥保存在安全的位置并向所有用户隐藏。当应用请求对账号中发布的应用进行许可检查时,许可服务器会使用应用密钥对的私钥签署许可响应。当 LVL 收到响应时,它会使用应用提供的公钥验证许可响应的签名。

如需向应用添加许可,必须获取应用的许可公钥并将其复制到应用中。下面介绍了如何查找应用的许可公钥:

  1. 转至 Google Play 管理中心,然后登录。 请确保登录要发布(或将要发布)许可应用的账号。
  2. 在应用详情页面中,找到 服务和 API 链接,然后点击该链接。
  3. 服务和 API 页面中,找到许可与应用内购买结算部分。许可公钥位于此应用的许可密钥字段中。

如需将公钥添加到应用中,只需将字段中的键字符串作为字符串变量 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 连接

最后,如需在应用 Context 更改之前清理 LVL,请从 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 通过提供声明单个方法 allowDeviceAccess()DeviceLimiter 接口支持每台设备的许可。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 问题跟踪器 市场许可项目问题跟踪器 与 LVL 源代码类和接口实现直接相关的错误和问题报告

如需了解如何向上文所列论坛发帖的一般信息,请参阅“开发者支持资源”页上的社区资源部分。

其他资源

LVL 包含的示例应用提供了如何在 MainActivity 类中启动许可检查和处理结果的完整示例。