许可参考

LVL 类和接口

表 1 列出了通过 Android SDK 提供的许可证验证库 (LVL) 中的所有源文件。所有文件都是 com.android.vending.licensing 包的一部分。

表 1. LVL 库类和接口摘要。

类别 名称 描述
许可证检查和结果 LicenseChecker 您实例化(或子类化)以启动许可证检查的类。
LicenseCheckerCallback 您实现以处理许可证检查结果的接口。
策略 策略 您实现以根据许可证响应确定是否允许访问应用程序的接口。
ServerManagedPolicy 默认的 Policy 实现。使用许可证服务器提供的设置来管理许可证数据的本地存储、许可证有效性和重试。
StrictPolicy 替代的 Policy 实现。仅根据服务器的直接许可证响应强制执行许可证。不进行缓存或请求重试。
数据混淆
(可选)
Obfuscator 如果您正在使用缓存许可证响应数据的 Policy(例如 ServerManagedPolicy)来实现的接口。应用混淆算法对正在写入或读取的数据进行编码和解码。
AESObfuscator 使用 AES 加密/解密算法对数据进行混淆/解混淆的默认 Obfuscator 实现。
设备限制
(可选)
DeviceLimiter 如果您想将应用的使用限制到特定设备,则实现的接口。从 LicenseValidator 调用。对于大多数应用,不建议实现 DeviceLimiter,因为它需要后端服务器,并且可能会导致用户丢失对已许可应用的访问权限,除非精心设计。
NullDeviceLimiter 默认的 DeviceLimiter 实现,它是一个无操作 (允许访问所有设备)。
库核心,无需集成 ResponseData 保存许可证响应字段的类。
LicenseValidator 解密和验证从许可证服务器收到的响应的类。
ValidationException 指示在验证 Obfuscator 管理的数据完整性时发生的错误的类。
PreferenceObfuscator 将混淆数据写入/读取到系统的 SharedPreferences 存储的实用程序类。
ILicensingService 许可证检查请求通过其传递到 Google Play 客户端的单向 IPC 接口。
ILicenseResultListener 应用通过其接收来自许可证服务器的异步响应的单向 IPC 回调实现。

服务器响应

表 2 列出了许可证服务器返回的所有许可证响应字段。

表 2. Google Play 服务器返回的许可证响应字段摘要。

字段 描述
responseCode 许可证服务器返回的响应代码。响应代码在 服务器响应代码 中概述。
signedData 保存许可证服务器返回的数据的字符串连接,如下所示:responseCode|nonce|packageName|versionCode|userId|timestamp:extras
  • responseCode:许可证服务器返回的响应代码。
  • nonce:请求的随机数标识符。
  • packageName:要检查许可证的应用的包名称。
  • versionCode:要检查许可证的应用的版本代码。
  • userId:每个应用的用户唯一 ID,其中同一用户在不同应用中获得不同的 ID。
  • timestamp:从 1970-01-01 00:00:00 UTC 的纪元到请求的毫秒数。
  • extras:协助应用许可证管理的其他信息。其他字段在 服务器响应其他信息 中概述。
signature 使用应用特定密钥对 signedData 的签名。

服务器响应代码

表 3 列出了许可证服务器支持的所有许可证响应代码。通常,应用应处理所有这些响应代码。默认情况下,LVL 中的 LicenseValidator 类为您提供所有必要的这些响应代码处理。

表 3. 许可证响应中 Google Play 服务器返回的响应代码摘要。

响应代码 整数表示 描述 已签名? 其他信息 注释
LICENSED 0 应用已获得用户的许可。用户已购买应用,或被授权下载和安装应用的 alpha 或 beta 版本。 VTGTGR 根据 Policy 约束允许访问。
LICENSED_OLD_KEY 2 应用已获得用户的许可,但有更新的应用版本可用,该版本使用不同的密钥签名。 VTGTGRUT 根据 Policy 约束选择性地允许访问。

可能表示已安装应用版本使用的密钥对无效或已泄露。应用可以根据需要允许访问,或通知用户有更新可用,并在升级前限制进一步使用。

NOT_LICENSED 1 应用未获得用户的许可。 不允许访问。
ERROR_CONTACTING_SERVER 257 本地错误 - Google Play 应用无法访问许可证服务器,可能是由于网络可用性问题。 根据 Policy 重试限制重试许可证检查。
ERROR_SERVER_FAILURE 4 服务器错误 - 服务器无法加载应用的密钥对以进行许可证检查。 根据 Policy 重试限制重试许可证检查。
ERROR_INVALID_PACKAGE_NAME 258 本地错误 - 应用请求检查未安装在设备上的包的许可证。 不要重试许可证检查。

通常由开发错误引起。

ERROR_NON_MATCHING_UID 259 本地错误 — 应用程序请求了一个软件包的许可证检查,但该软件包的 UID(软件包、用户 ID 对)与请求应用程序的 UID 不匹配。 不要重试许可证检查。

通常由开发错误引起。

ERROR_NOT_MARKET_MANAGED 3 服务器错误 — Google Play 未识别应用程序(软件包名称)。 不要重试许可证检查。

可能表示该应用程序不是通过 Google Play 发布的,或者许可证实现中存在开发错误。

注意:设置测试环境 中所述,可以通过 Google Play Console 手动覆盖应用程序开发者和任何已注册的测试用户的响应代码。

注意:以前可以通过上传未发布的“草稿”版本来测试应用。此功能不再受支持;您必须将其发布到 Alpha 或 Beta 分发渠道。有关更多信息,请参阅 不再支持草稿应用

服务器响应额外信息

为了帮助您的应用程序在应用程序退款期间管理对应用程序的访问并提供其他信息,许可证服务器在许可证响应中包含了一些信息。具体来说,该服务为应用程序的许可证有效期、重试宽限期、最大允许重试次数和其他设置提供了推荐值。如果您的应用程序使用 APK 扩展文件,则响应还包括文件名、大小和 URL。服务器将设置作为键值对附加到许可证响应的“额外信息”字段中。

任何 Policy 实现都可以从许可证响应中提取额外信息设置,并根据需要使用它们。LVL 默认 Policy 实现,ServerManagedPolicy,作为工作实现以及如何获取、存储和使用设置的示例。

表 4. Google Play 服务器在许可证响应中提供的许可证管理设置摘要。

额外信息描述
VT 许可证有效期时间戳。指定当前(缓存)许可证响应过期并必须在许可证服务器上重新检查的日期/时间。请参阅下面关于 许可证有效期 的部分。
GT 宽限期时间戳。指定策略允许访问应用程序的期间的结束时间,即使响应状态为 RETRY

该值由服务器管理,但典型值为 5 天或更长时间。请参阅下面关于 重试期间和最大重试次数 的部分。

GR 最大重试次数。指定 Policy 在拒绝用户访问应用程序之前应允许多少次连续的 RETRY 许可证检查。

该值由服务器管理,但典型值为“10”或更高。请参阅下面关于 重试期间和最大重试次数 的部分。

UT 更新时间戳。指定此应用程序的最新更新上传和发布的日期/时间。

服务器仅针对 LICENSED_OLD_KEYS 响应返回此额外信息,以允许 Policy 确定在发布具有新许可证密钥的更新后经过了多长时间,然后再拒绝用户访问应用程序。

FILE_URL1FILE_URL2 扩展文件的 URL(1 用于主文件,2 用于补丁文件)。使用此 URL 通过 HTTP 下载文件。
FILE_NAME1FILE_NAME2 扩展文件的文件名(1 用于主文件,2 用于补丁文件)。保存文件到设备时必须使用此名称。
FILE_SIZE1FILE_SIZE2 文件的大小(以字节为单位)(1 用于主文件,2 用于补丁文件)。使用此信息来帮助下载并确保在下载之前设备的共享存储位置上有足够的空间。

许可证有效期

Google Play 许可证服务器为所有下载的应用程序设置许可证有效期。该期间表示应用程序的许可证状态应被视为不变且可由应用程序中的许可证 Policy 缓存的时间间隔。许可证服务器在其对所有许可证检查的响应中包含有效期,并将有效期结束时间戳作为额外信息附加到响应中,键为 VT。一个 Policy 可以提取 VT 键值并使用它有条件地允许访问应用程序,而无需重新检查许可证,直到有效期过期。

许可证有效期向许可证 Policy 发出信号,指示它必须何时与许可证服务器重新检查许可证状态。它并非旨在暗示应用程序是否已获得使用许可。也就是说,当应用程序的许可证有效期过期时,并不意味着该应用程序不再获得使用许可——而仅表示 Policy 必须与服务器重新检查许可证状态。因此,只要许可证有效期尚未过期,Policy 就可以在本地缓存初始许可证状态,并返回缓存的许可证状态,而不是向服务器发送新的许可证检查。

许可证服务器管理有效期,作为帮助应用程序在 Google Play 为付费应用程序提供的退款期间正确执行许可证的一种手段。它根据应用程序是否已购买以及(如果是)购买了多长时间来设置有效期。具体来说,服务器按如下方式设置有效期

  • 对于付费应用程序,服务器设置初始许可证有效期,以便许可证响应在应用程序可退款的整个期间内保持有效。应用程序中的许可证 Policy 可以缓存初始许可证检查的结果,并且无需在有效期过期之前重新检查许可证。
  • 当应用程序不再可退款时,服务器会设置更长的有效期——通常为几天。
  • 对于免费应用程序,服务器将有效期设置为一个非常高的值(long.MAX_VALUE)。这确保了,只要 Policy 已在本地缓存了有效期时间戳,将来就不需要重新检查应用程序的许可证状态。

ServerManagedPolicy 实现使用提取的时间戳(mValidityTimestamp)作为确定是否在允许用户访问应用程序之前与服务器重新检查许可证状态的主要条件。

重试期间和最大重试次数

在某些情况下,系统或网络状况可能会阻止应用程序的许可证检查到达许可证服务器,或阻止服务器的响应到达 Google Play 客户端应用程序。例如,用户可能在没有蜂窝网络或数据连接的情况下(例如在飞机上)或网络连接不稳定或蜂窝信号较弱时启动应用程序。

当网络问题阻止或中断许可证检查时,Google Play 客户端会通过将 RETRY 响应代码返回到 PolicyprocessServerResponse() 方法来通知应用程序。在系统问题的情况下,例如当应用程序无法绑定到 Google Play 的 ILicensingService 实现时,LicenseChecker 库本身会使用 RETRY 响应代码调用 Policy processServerResponse() 方法。

通常,RETRY 响应代码是向应用程序发出信号,表明发生了错误,从而阻止了许可证检查的完成。

Google Play 服务器通过设置重试“宽限期”和推荐的最大重试次数来帮助应用程序在错误情况下管理许可证。服务器在所有许可证检查响应中包含这些值,并将它们作为额外信息附加到键 GTGR 下。

应用程序 Policy 可以提取 GTGR 额外信息,并使用它们有条件地允许访问应用程序,如下所示

  • 对于导致 RETRY 响应的许可证检查,Policy 应缓存 RETRY 响应代码并增加 RETRY 响应的计数。
  • Policy 应允许用户访问应用程序,前提是重试宽限期仍然有效或尚未达到最大重试次数。

ServerManagedPolicy 使用服务器提供的 GTGR 值,如上所述。下面的示例显示了在 allow() 方法中对重试响应的条件处理。RETRY 响应的计数在 processServerResponse() 方法中维护,此处未显示。

Kotlin

fun allowAccess(): Boolean {
    val ts = System.currentTimeMillis()
    return when(lastResponse) {
        LICENSED -> {
            // Check if the LICENSED response occurred within the validity timeout.
            ts <= validityTimestamp  // Cached LICENSED response is still valid.
        }
        RETRY -> {
            ts < lastResponseTime + MILLIS_PER_MINUTE &&
                    // Only allow access if we are within the retry period
                    // or we haven't used up our max retries.
                    (ts <= retryUntil || retryCount <= maxRetries)
        }
        else -> false
    }
}

Java

public boolean allowAccess() {
    long ts = System.currentTimeMillis();
    if (lastResponse == LicenseResponse.LICENSED) {
        // Check if the LICENSED response occurred within the validity timeout.
        if (ts <= validityTimestamp) {
            // Cached LICENSED response is still valid.
            return true;
        }
    } else if (lastResponse == LicenseResponse.RETRY &&
                ts < lastResponseTime + MILLIS_PER_MINUTE) {
        // Only allow access if we are within the retry period
        // or we haven't used up our max retries.
        return (ts <= retryUntil || retryCount <= maxRetries);
    }
    return false;
}