许可参考

LVL 类和接口

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

表 1. LVL 库类和接口汇总。

类别 名称 描述
许可证检查和结果 LicenseChecker 您实例化(或子类化)以启动许可证检查的类。
LicenseCheckerCallback 您实现的接口,用于处理许可证检查的结果。
策略 策略 您实现的接口,用于根据许可证响应确定是否允许访问应用程序。
ServerManagedPolicy 默认 Policy 实现。使用许可证服务器提供的设置来管理许可证数据的本地存储、许可证有效性、重试。
StrictPolicy 替代的 Policy 实现。仅根据服务器的直接许可证响应强制执行许可证。没有缓存或请求重试。
数据混淆
(可选)
Obfuscator 如果您使用的是 Policy(例如 ServerManagedPolicy)在持久性存储中缓存许可证响应数据,则实现的接口。应用混淆算法对要写入或读取的数据进行编码和解码。
AESObfuscator 默认 Obfuscator 实现,使用 AES 加密/解密算法来混淆/取消混淆数据。
设备限制
(可选)
DeviceLimiter 如果您想将应用程序的使用限制在特定设备上,则实现的接口。从 LicenseValidator 调用。对于大多数应用程序,不建议实现 DeviceLimiter,因为它需要后端服务器,并且可能会导致用户失去对许可应用程序的访问权限,除非经过精心设计。
NullDeviceLimiter 默认 DeviceLimiter 实现,它是无操作的(允许访问所有设备)。
库核心,无需集成 ResponseData 保存许可证响应字段的类。
LicenseValidator 用于解密和验证从许可服务器收到的响应的类。
ValidationException 表示在验证混淆器管理的数据完整性时发生的错误的类。
PreferenceObfuscator 实用程序类,用于将混淆数据写入/读取系统的 SharedPreferences 存储。
ILicensingService 单向 IPC 接口,通过该接口将许可证检查请求传递到 Google Play 客户端。
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 版本。 VTGT, GR 根据 Policy 约束允许访问。
LICENSED_OLD_KEY 2 该应用程序已获得用户的许可,但有可用的更新应用版本,该版本使用不同的密钥签名。 VT, GT, GR, UT 可以选择根据 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 管理控制台手动覆盖应用程序开发人员和任何注册的测试用户的响应代码。

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

服务器响应附加信息

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

任何 Policy 实现都可以从许可证响应中提取附加信息设置并根据需要使用它们。LVL 默认 Policy 实现,ServerManagedPolicy,用作工作实现并说明如何获取、存储和使用设置。

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

附加信息描述
VT 许可证有效期时间戳。指定当前(缓存)许可证响应过期的时间,必须在许可服务器上重新检查。请参阅下面关于 许可证有效期 的部分。
GT 宽限期时间戳。指定 Policy 即使响应状态为 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 库本身会调用 Policy processServerResponse() 方法,并使用 RETRY 响应代码。

通常,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;
}