LVL 类和接口
表 1 列出了通过 Android SDK 提供的许可验证库 (LVL) 中的所有源文件。所有文件都属于 com.android.vending.licensing
软件包。
表 1. LVL 库类和接口摘要。
类别 | 名称 | 说明 |
---|---|---|
许可检查和结果 | LicenseChecker | 您实例化(或子类化)以启动许可检查的类。 |
LicenseCheckerCallback | 您实现以处理许可检查结果的接口。 | |
Policy | Policy | 您根据许可响应确定是否允许访问应用程序而实现的接口。 |
ServerManagedPolicy | 默认的 Policy 实现。使用许可服务器提供的设置来管理许可数据的本地存储、许可有效期、重试。 |
|
StrictPolicy | 备选的 Policy 实现。仅根据服务器的直接许可响应强制执行许可。无缓存或请求重试。 |
|
数据混淆 (可选) |
Obfuscator | 如果您正在使用缓存许可响应数据到持久存储中的 Policy (例如 ServerManagedPolicy),则需要实现的接口。应用混淆算法来编码和解码正在写入或读取的数据。 |
AESObfuscator | 默认的 Obfuscator 实现,使用 AES 加密/解密算法来混淆/去混淆数据。 | |
设备限制 (可选) |
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 。
|
signature |
使用应用特定密钥对 signedData 的签名。 |
服务器响应代码
表 3 列出了许可服务器支持的所有许可响应代码。通常,应用程序应处理所有这些响应代码。默认情况下,LVL 中的 LicenseValidator 类为您提供了所有这些响应代码的必要处理。
表 3. Google Play 服务器在许可响应中返回的响应代码摘要。
响应代码 | 整数值表示 | 说明 | 已签名? | 额外信息 | 注释 |
---|---|---|---|---|---|
LICENSED |
0 |
该应用程序已授权给用户。用户已购买该应用程序,或获授权下载和安装该应用程序的 Alpha 或 Beta 版本。 | 是 | VT 、 GT 、 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。服务器将这些设置作为键值对附加到许可响应的“extras”字段中。
任何 Policy
实现都可以从许可响应中提取额外设置并根据需要使用它们。LVL 默认的 Policy
实现 ServerManagedPolicy
,作为一个可行的实现,并说明了如何获取、存储和使用这些设置。
表 4. Google Play 服务器在许可响应中提供的许可管理设置摘要。
额外信息 | 说明 |
---|---|
VT |
许可有效期时间戳。指定当前(缓存的)许可响应到期并必须在许可服务器上重新检查的日期/时间。请参阅下面关于许可有效期的部分。 |
GT |
宽限期时间戳。指定在此期间,即使响应状态为 RETRY ,Policy 仍可能允许访问应用程序的结束时间。该值由服务器管理,但典型值将是 5 天或更长时间。请参阅下面关于重试期和最大重试次数的部分。 |
GR |
最大重试次数。指定 Policy 在拒绝用户访问应用程序之前应允许多少次连续的 RETRY 许可检查。该值由服务器管理,但典型值将是“10”或更高。请参阅下面关于重试期和最大重试次数的部分。 |
UT |
更新时间戳。指定此应用程序最近一次更新上传和发布的日期/时间。 服务器仅对 |
FILE_URL1 或 FILE_URL2 |
扩展文件的 URL(1 用于主文件,2 用于补丁文件)。使用此 URL 通过 HTTP 下载文件。 |
FILE_NAME1 或 FILE_NAME2 |
扩展文件的名称(1 用于主文件,2 用于补丁文件)。在设备上保存文件时必须使用此名称。 |
FILE_SIZE1 或 FILE_SIZE2 |
文件大小(以字节为单位)(1 用于主文件,2 用于补丁文件)。使用此信息协助下载并确保在下载之前设备的共享存储位置有足够的空间。 |
许可有效期
Google Play 许可服务器为所有下载的应用程序设置许可有效期。该期限表示应用程序的许可状态应被应用程序中的许可 Policy
视为不变且可缓存的时间间隔。许可服务器在其所有许可检查的响应中都包含有效期,将有效期结束时间戳作为额外信息附加到响应中,键为 VT
。A Policy
可以提取 VT 键值并使用它有条件地允许访问应用程序,而无需重新检查许可,直到有效期过期。
许可有效期向许可 Policy
发出信号,指示何时必须与许可服务器重新检查许可状态。它不意味着应用程序是否实际已获许可使用。也就是说,当应用程序的许可有效期过期时,这并不意味着应用程序不再获许可使用 — 相反,它仅表示 Policy
必须与服务器重新检查许可状态。因此,只要许可有效期未过期,Policy
就可以在本地缓存初始许可状态并返回缓存的许可状态,而不是向服务器发送新的许可检查。
许可服务器管理有效期,作为帮助应用程序在 Google Play 为付费应用程序提供的退款期内正确执行许可的一种方式。它根据应用程序是否已购买以及购买了多久来设置有效期。具体来说,服务器按如下方式设置有效期:
- 对于付费应用程序,服务器设置初始许可有效期,以便许可响应在应用程序可退款期间内保持有效。应用程序中的许可
Policy
可以缓存初始许可检查的结果,并且无需重新检查许可,直到有效期过期。 - 当应用程序不再可退款时,服务器会设置更长的有效期——通常是几天。
- 对于免费应用程序,服务器将有效期设置为非常高的值 (
long.MAX_VALUE
)。这确保了,只要Policy
在本地缓存了有效期时间戳,未来就无需重新检查应用程序的许可状态。
ServerManagedPolicy
实现使用提取的时间戳 (mValidityTimestamp
) 作为主要条件,用于确定在允许用户访问应用程序之前是否与服务器重新检查许可状态。
重试期和最大重试次数
在某些情况下,系统或网络条件可能会阻止应用程序的许可检查到达许可服务器,或者阻止服务器的响应到达 Google Play 客户端应用程序。例如,用户可能在没有蜂窝网络或数据连接可用时(例如在飞机上)或网络连接不稳定或蜂窝信号弱时启动应用程序。
当网络问题阻止或中断许可检查时,Google Play 客户端会通过向 Policy
的 processServerResponse()
方法返回 RETRY
响应代码来通知应用程序。在系统问题的情况下,例如当应用程序无法绑定到 Google Play 的 ILicensingService
实现时,LicenseChecker
库本身会使用 RETRY
响应代码调用 Policy processServerResponse()
方法。
一般来说,RETRY
响应代码是向应用程序发出的信号,表明发生了阻止许可检查完成的错误。
Google Play 服务器通过设置重试“宽限期”和建议的最大重试次数来帮助应用程序在错误条件下管理许可。服务器在所有许可检查响应中都包含这些值,并将它们作为额外信息附加到 GT
和 GR
键下。
应用程序 Policy
可以提取 GT
和 GR
额外信息,并使用它们有条件地允许访问应用程序,如下所示:
- 对于导致
RETRY
响应的许可检查,Policy
应缓存RETRY
响应代码并增加RETRY
响应计数。 Policy
应允许用户访问应用程序,前提是重试宽限期仍然有效或尚未达到最大重试次数。
ServerManagedPolicy
使用服务器提供的 GT
和 GR
值,如上所述。下面的示例展示了 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; }