本页面介绍了如何解读和使用返回的完整性判定结果。无论您发出标准 API 请求还是传统 API 请求,系统都会以相同的格式返回内容相似的完整性判定结果。完整性判定用于传达与设备、应用和账号有效性相关的信息。已解密且经过验证的判定中包含所生成的载荷,应用服务器可使用此载荷来确定如何以最佳方式继续推进应用中的特定操作或请求。
返回的完整性判定结果的格式
载荷是纯文本 JSON,其中包含完整性信号以及开发者提供的信息。
常规载荷结构如下:
{ requestDetails: { ... } appIntegrity: { ... } deviceIntegrity: { ... } accountDetails: { ... } environmentDetails: { ... } }
您必须先检查 requestDetails
字段中的值是否与原始请求的值相符,然后再检查每个完整性判定结果。下面几部分详细介绍了各个字段。
请求详情字段
requestDetails
字段包含与请求相关的信息,其中包括 requestHash
(适用于标准请求)和 nonce
(适用于传统请求)中由开发者提供的信息。
对于标准 API 请求:
requestDetails: { // Application package name this attestation was requested for. // Note that this field might be spoofed in the middle of the request. requestPackageName: "com.package.name" // Request hash provided by the developer. requestHash: "aGVsbG8gd29scmQgdGhlcmU" // The timestamp in milliseconds when the integrity token // was requested. timestampMillis: "1675655009345" }
这些值应与原始请求中的值相符。因此,请验证 JSON 载荷的 requestDetails
部分,确保 requestPackageName
和 requestHash
与原始请求中发送的内容相符,如以下代码段所示:
Kotlin
val requestDetails = JSONObject(payload).getJSONObject("requestDetails") val requestPackageName = requestDetails.getString("requestPackageName") val requestHash = requestDetails.getString("requestHash") val timestampMillis = requestDetails.getLong("timestampMillis") val currentTimestampMillis = ... // Ensure the token is from your app. if (!requestPackageName.equals(expectedPackageName) // Ensure the token is for this specific request || !requestHash.equals(expectedRequestHash) // Ensure the freshness of the token. || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) { // The token is invalid! See below for further checks. ... }
Java
RequestDetails requestDetails = decodeIntegrityTokenResponse .getTokenPayloadExternal() .getRequestDetails(); String requestPackageName = requestDetails.getRequestPackageName(); String requestHash = requestDetails.getRequestHash(); long timestampMillis = requestDetails.getTimestampMillis(); long currentTimestampMillis = ...; // Ensure the token is from your app. if (!requestPackageName.equals(expectedPackageName) // Ensure the token is for this specific request. || !requestHash.equals(expectedRequestHash) // Ensure the freshness of the token. || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) { // The token is invalid! See below for further checks. ... }
对于传统 API 请求:
requestDetails: { // Application package name this attestation was requested for. // Note that this field might be spoofed in the middle of the // request. requestPackageName: "com.package.name" // base64-encoded URL-safe no-wrap nonce provided by the developer. nonce: "aGVsbG8gd29scmQgdGhlcmU" // The timestamp in milliseconds when the request was made // (computed on the server). timestampMillis: "1617893780" }
这些值应与原始请求中的值相符。因此,请验证 JSON 载荷的 requestDetails
部分,确保 requestPackageName
和 nonce
与原始请求中发送的内容相符,如以下代码段所示:
Kotlin
val requestDetails = JSONObject(payload).getJSONObject("requestDetails") val requestPackageName = requestDetails.getString("requestPackageName") val nonce = requestDetails.getString("nonce") val timestampMillis = requestDetails.getLong("timestampMillis") val currentTimestampMillis = ... // Ensure the token is from your app. if (!requestPackageName.equals(expectedPackageName) // Ensure the token is for this specific request. See 'Generate a nonce' // section of the doc on how to store/compute the expected nonce. || !nonce.equals(expectedNonce) // Ensure the freshness of the token. || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) { // The token is invalid! See below for further checks. ... }
Java
JSONObject requestDetails = new JSONObject(payload).getJSONObject("requestDetails"); String requestPackageName = requestDetails.getString("requestPackageName"); String nonce = requestDetails.getString("nonce"); long timestampMillis = requestDetails.getLong("timestampMillis"); long currentTimestampMillis = ...; // Ensure the token is from your app. if (!requestPackageName.equals(expectedPackageName) // Ensure the token is for this specific request. See 'Generate a nonce' // section of the doc on how to store/compute the expected nonce. || !nonce.equals(expectedNonce) // Ensure the freshness of the token. || currentTimestampMillis - timestampMillis > ALLOWED_WINDOW_MILLIS) { // The token is invalid! See below for further checks. ... }
应用完整性字段
appIntegrity
字段包含与软件包相关的信息。
appIntegrity: { // PLAY_RECOGNIZED, UNRECOGNIZED_VERSION, or UNEVALUATED. appRecognitionVerdict: "PLAY_RECOGNIZED" // The package name of the app. // This field is populated iff appRecognitionVerdict != UNEVALUATED. packageName: "com.package.name" // The sha256 digest of app certificates (base64-encoded URL-safe). // This field is populated iff appRecognitionVerdict != UNEVALUATED. certificateSha256Digest: ["6a6a1474b5cbbb2b1aa57e0bc3"] // The version of the app. // This field is populated iff appRecognitionVerdict != UNEVALUATED. versionCode: "42" }
appRecognitionVerdict
的值可能如下:
PLAY_RECOGNIZED
- 应用和证书与 Google Play 分发的版本相符。
UNRECOGNIZED_VERSION
- 证书或软件包名称与 Google Play 记录不符。
UNEVALUATED
- 未评估应用完整性。未达成必要条件,例如设备不够可信。
如需确保令牌是由您创建的应用生成的,请验证应用完整性是否符合预期,如以下代码段所示:
Kotlin
val appIntegrity = JSONObject(payload).getJSONObject("appIntegrity") val appRecognitionVerdict = appIntegrity.getString("appRecognitionVerdict") if (appRecognitionVerdict == "PLAY_RECOGNIZED") { // Looks good! }
Java
JSONObject appIntegrity = new JSONObject(payload).getJSONObject("appIntegrity"); String appRecognitionVerdict = appIntegrity.getString("appRecognitionVerdict"); if (appRecognitionVerdict.equals("PLAY_RECOGNIZED")) { // Looks good! }
您也可以手动检查应用软件包名称、应用版本和应用证书。
设备完整性字段
deviceIntegrity
字段可以包含单个值 deviceRecognitionVerdict
;该值具有一个或多个标签,用来表示设备可以在多大程度上满足执行应用完整性检查的条件。如果设备不满足任何标签的条件,deviceIntegrity
字段将为空。
deviceIntegrity: { // "MEETS_DEVICE_INTEGRITY" is one of several possible values. deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"] }
默认情况下,deviceRecognitionVerdict
可包含以下内容:
MEETS_DEVICE_INTEGRITY
- 应用正在已安装 Google Play 服务的 Android 设备上运行。设备通过了系统完整性检查,并且满足 Android 兼容性要求。
- 空(空白值)
- 运行应用的设备存在遭受攻击(如 API 挂接)或系统被入侵(如取得 root 权限后入侵)的迹象,或者不是实体设备(如未通过 Google Play 完整性检查的模拟器)。
为确保令牌来自可信设备,请验证 deviceRecognitionVerdict
是否符合预期,如以下代码段所示:
Kotlin
val deviceIntegrity = JSONObject(payload).getJSONObject("deviceIntegrity") val deviceRecognitionVerdict = if (deviceIntegrity.has("deviceRecognitionVerdict")) { deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString() } else { "" } if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) { // Looks good! }
Java
JSONObject deviceIntegrity = new JSONObject(payload).getJSONObject("deviceIntegrity"); String deviceRecognitionVerdict = deviceIntegrity.has("deviceRecognitionVerdict") ? deviceIntegrity.getJSONArray("deviceRecognitionVerdict").toString() : ""; if (deviceRecognitionVerdict.contains("MEETS_DEVICE_INTEGRITY")) { // Looks good! }
如果您发现所用的测试设备不能满足完整性要求,请确保已安装出厂 ROM(例如,通过重置设备),并且引导加载程序处于锁定状态。您还可以在 Play 管理中心内创建 Play Integrity API 测试。
条件设备标签
如果您的应用将发布到 Google Play Games 电脑版,deviceRecognitionVerdict
还可包含以下标签:
MEETS_VIRTUAL_INTEGRITY
- 应用正在已安装 Google Play 服务的 Android 模拟器上运行。模拟器通过了系统完整性检查,并且满足核心 Android 兼容性要求。
可选设备信息
如果您选择在完整性判定结果中接收其他标签,则 deviceRecognitionVerdict
可能会包含以下额外标签:
MEETS_BASIC_INTEGRITY
- 应用正在已通过基本系统完整性检查的设备上运行。设备可能不满足 Android 兼容性要求,也可能未被批准运行 Google Play 服务。例如,设备运行的可能是无法识别的 Android 版本、设备的引导加载程序可能已遭解锁,或者设备可能没有经过制造商的认证。
MEETS_STRONG_INTEGRITY
- 应用在已安装 Google Play 服务且具有强有力的系统完整性保证(如由硬件提供支持的启动完整性保证)的 Android 设备上运行。设备通过了系统完整性检查,并且满足 Android 兼容性要求。
如果满足标签的每个条件,对于同一部设备,设备完整性判定会返回多个设备标签。
近期设备活动记录
您还可以选择启用近期设备活动记录,了解过去一小时内您的应用在特定设备上请求完整性令牌的次数。您可以利用近期设备活动记录来保护您的应用免受意外超活跃设备的影响,意外超活跃设备可能表明存在主动攻击。您可以根据预计在典型设备上安装的应用每小时请求完整性令牌的次数,确定对每个近期设备活动记录级别的信任程度。
如果您选择接收 recentDeviceActivity
,则 deviceIntegrity
字段将有两个值:
deviceIntegrity: { deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"] recentDeviceActivity: { // "LEVEL_2" is one of several possible values. deviceActivityLevel: "LEVEL_2" } }
deviceActivityLevel
定义因模式而异,可以是以下某个值:
近期设备活动记录级别 | 此设备上过去一小时内每个应用的标准 API 完整性令牌请求数 | 过去一小时内每个应用在此设备上的传统 API 完整性令牌请求数 |
---|---|---|
LEVEL_1 (最低) |
10 个或 10 个以下 | 5 个或更少 |
LEVEL_2 |
介于 11 到 25 之间 | 介于 6 到 10 之间 |
LEVEL_3 |
26 到 50 个 | 11 到 15 个 |
LEVEL_4 (最高) |
50 个以上 | 超过 15 个 |
UNEVALUATED |
未评估近期设备活动记录。出现这种情况的原因可能是:
|
账号详情字段
accountDetails
字段包含单个值 appLicensingVerdict
,它表示在设备上登录的用户账号的应用的 Google Play 许可状态。如果用户账号拥有应用的 Play 许可,则表示用户从 Google Play 下载或购买了该应用。
accountDetails: { // This field can be LICENSED, UNLICENSED, or UNEVALUATED. appLicensingVerdict: "LICENSED" }
appLicensingVerdict
可采用以下值之一:
LICENSED
- 用户拥有应用使用权。换句话说,用户在其设备上从 Google Play 安装或更新了您的应用。
UNLICENSED
- 用户没有应用使用权。例如,如果用户旁加载了您的应用,或不是通过 Google Play 获取您的应用,就会发生这种情况。您可以向用户显示 GET_LICENSED 对话框来加以补救。
UNEVALUATED
由于未达成必要条件,系统未能评估许可详情。
导致这种情况的原因可能有多种,其中包括以下原因:
- 设备不够可信。
- 设备上安装的应用是 Google Play 未知的版本。
- 用户未登录 Google Play。
如需检查用户是否拥有应用的使用权,请验证 appLicensingVerdict
是否符合预期,如以下代码段所示:
Kotlin
val accountDetails = JSONObject(payload).getJSONObject("accountDetails") val appLicensingVerdict = accountDetails.getString("appLicensingVerdict") if (appLicensingVerdict == "LICENSED") { // Looks good! }
Java
JSONObject accountDetails = new JSONObject(payload).getJSONObject("accountDetails"); String appLicensingVerdict = accountDetails.getString("appLicensingVerdict"); if (appLicensingVerdict.equals("LICENSED")) { // Looks good! }
环境详情字段
您还可以选择接收有关环境的其他信号。“应用访问风险”信号可告知您的应用是否有其他正在运行的应用可能会截屏、显示叠加层或控制设备。Play 保护机制判定结果可告知您设备上是否已启用 Google Play 保护机制,以及是否发现了已知恶意软件。
如果您已在 Google Play 管理中心内选择启用应用访问风险信号判定或 Play 保护机制判定,则您的 API 响应将包含 environmentDetails
字段。environmentDetails
字段可以包含两个值:appAccessRiskVerdict
和 playProtectVerdict
。
应用访问风险判定结果(Beta 版)
启用后,Play Integrity API 载荷中的 environmentDetails
字段将包含新的应用访问风险判定结果。
{
requestDetails: { ... }
appIntegrity: { ... }
deviceIntegrity: { ... }
accountDetails: { ... }
environmentDetails: {
appAccessRiskVerdict: {
// This field contains one or more responses, for example the following.
appsDetected: ["KNOWN_INSTALLED", "UNKNOWN_INSTALLED", "UNKNOWN_CAPTURING"]
}
}
}
如果已评估应用访问风险,appAccessRiskVerdict
将包含一个或多个响应的 appsDetected
字段。这些响应会根据检测到的应用的安装来源分为以下两组之一:
Play 应用或系统应用:通过 Google Play 安装的应用或由设备制造商在设备的系统分区中预加载的应用(使用
FLAG_SYSTEM
标识)。此类应用的响应带有KNOWN_
前缀。其他应用:非通过 Google Play 安装的应用。此字段会排除设备制造商在系统分区中预加载的应用。此类应用的响应前缀为
UNKNOWN_
。
系统可能会返回以下响应:
KNOWN_INSTALLED
、UNKNOWN_INSTALLED
- 有与相应安装来源匹配的已安装应用。
KNOWN_CAPTURING
、UNKNOWN_CAPTURING
- 表明有正在运行的应用启用了相应权限,可用于在应用运行时查看屏幕。这会排除设备上运行的任何 Google Play 已知的经验证无障碍服务。
KNOWN_CONTROLLING
、UNKNOWN_CONTROLLING
- 某些正在运行的应用启用了相关权限,可用于控制设备并直接控制输入到您的应用中的内容,还可用于捕获应用的输入和输出内容。这不包括 Google Play 已知在设备上运行的任何经过验证的无障碍服务。
KNOWN_OVERLAYS
、UNKNOWN_OVERLAYS
- 表明有运行中的应用启用了相应权限,可用于在您的应用上显示叠加层。这会排除设备上运行的任何 Google Play 已知的经验证无障碍服务。
- 空(空白值)
如果未达成必要条件,系统将不会评估应用访问风险。在这种情况下,
appAccessRiskVerdict
字段为空。导致这种情况的原因可能有多种,其中包括:- 设备不够可信。
- 设备外形规格不是手机、平板电脑或可折叠设备。
- 设备未搭载 Android 6(API 级别 23)或更高版本。
- 设备上安装的应用是 Google Play 未知的版本。
- 设备上的 Google Play 商店版本已过时。
- 仅限游戏:用户账号不具备相应游戏的 Play 许可。
- 使用了包含
verdictOptOut
参数的标准请求。 - 使用的标准请求与尚不支持标准请求的应用访问风险信号的 Play Integrity API 库版本搭配使用。
应用访问风险信号会自动排除已通过增强型 Google Play 无障碍功能审核且已经验证的无障碍服务(无论设备上的安装来源如何)。“已排除”意味着,对于设备上运行的经验证无障碍服务,应用访问风险判定结果中不会返回 CAPTURING、CONTROLLING 或 OVERLAYS 响应。如需为无障碍应用申请增强型 Google Play 无障碍功能审核,请在 Google Play 上发布该应用,并确保在应用清单中将 isAccessibilityTool
标志设置为 true,或者申请审核。
下表列举了一些示例来说明判定结果及其含义(下表并未列出所有可能的结果):
应用访问风险判定结果响应示例 | 解读 |
---|---|
appsDetected: ["KNOWN_INSTALLED"]
|
只有可被 Google Play 识别或被设备制造商预加载到系统分区中的已安装应用。 没有任何正在运行的应用会导致“CAPTURING”“CONTROLLING”或“OVERLAYS”判定结果。 |
appsDetected: ["KNOWN_INSTALLED", "UNKNOWN_INSTALLED", "UNKNOWN_CAPTURING"]
|
表明有 Google Play 安装的应用或设备制造商在系统分区中预加载的应用。 还有其他应用在运行且已启用相关权限,这些应用可用于查看屏幕或捕获其他输入和输出内容。 |
appsDetected: ["KNOWN_INSTALLED", "KNOWN_CAPTURING", "UNKNOWN_INSTALLED", "UNKNOWN_CONTROLLING"]
|
表明有正在运行的 Play 应用或系统应用,且应用已启用相应权限并可用于查看屏幕上的内容或捕获其他输入和输出内容。 表明还有其他应用在运行,且应用已启用相应权限并可用于控制设备并直接控制输入到您的应用中的内容。 |
appAccessRiskVerdict: {}
|
由于未达成必要条件,系统未能评估应用访问风险。例如,设备不够可信。 |
您可根据自己的风险级别来决定接受哪些判定结果组合以执行后续操作,以及您要针对哪些判定结果采取措施。以下代码段是一个示例,展示了如何验证没有正在运行且可截屏或控制您应用的应用:
Kotlin
val environmentDetails = JSONObject(payload).getJSONObject("environmentDetails") val appAccessRiskVerdict = environmentDetails.getJSONObject("appAccessRiskVerdict") if (appAccessRiskVerdict.has("appsDetected")) { val appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString() if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) { // Looks good! } }
Java
JSONObject environmentDetails = new JSONObject(payload).getJSONObject("environmentDetails"); JSONObject appAccessRiskVerdict = environmentDetails.getJSONObject("appAccessRiskVerdict"); if (appAccessRiskVerdict.has("appsDetected")) { String appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString() if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) { // Looks good! } }
修复应用访问风险判定结果
您可根据自己的风险级别来决定在允许用户完成请求或操作之前,要针对哪些应用访问风险信号判定结果采取措施。在检查应用访问风险判定结果后,您可以向用户显示可选的 Google Play 提示。您可以显示 CLOSE_UNKNOWN_ACCESS_RISK 以要求用户关闭导致应用访问风险判定结果的未知应用,也可以显示 CLOSE_ALL_ACCESS_RISK 以要求用户关闭导致应用访问风险判定结果的所有应用(已知和未知应用)。
Play 保护机制判定结果
启用后,Play Integrity API 载荷中的 environmentDetails
字段将包含 Play 保护机制判定结果:
environmentDetails: {
playProtectVerdict: "NO_ISSUES"
}
playProtectVerdict
可采用以下值之一:
NO_ISSUES
- Play 保护机制处于开启状态,但在设备上未发现任何应用问题。
NO_DATA
- Play 保护机制处于开启状态,但尚未执行任何扫描。设备或 Play 商店应用可能在近期重置了。
POSSIBLE_RISK
- Play 保护机制处于关闭状态。
MEDIUM_RISK
- Play 保护机制处于开启状态,并且在设备上发现了可能有害的已安装应用。
HIGH_RISK
- Play 保护机制处于开启状态,并在设备上发现了危险的已安装应用。
UNEVALUATED
未评估 Play 保护机制判定结果。
导致这种情况的原因可能有多种,其中包括以下原因:
- 设备不够可信。
- 仅限游戏:用户账号不具备相应游戏的 Play 许可。
Play 保护机制判定使用指南
应用的后端服务器可基于您的风险容忍度来决定如何应对。以下是一些建议和可行的用户操作:
NO_ISSUES
- Play 保护机制处于开启状态,但未发现任何问题,因此用户无需执行任何操作。
- “
POSSIBLE_RISK
”和“NO_DATA
” - 收到这些判定后,要求用户检查 Play 保护机制是否处于开启状态以及是否已执行扫描。
NO_DATA
应仅在极少数情况下显示。 - “
MEDIUM_RISK
”和“HIGH_RISK
” - 您可根据自己的风险容忍度,要求用户启动 Play 保护机制并基于 Play 保护机制警告采取相应措施。如果用户无法满足这些要求,您可以阻止他们执行服务器操作。