完整性判定

本页面介绍了如何解读和使用返回的完整性判定结果。无论您发出标准 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 部分,确保 requestPackageNamerequestHash 与原始请求中发送的内容相符,如以下代码段所示:

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 部分,确保 requestPackageNamenonce 与原始请求中发送的内容相符,如以下代码段所示:

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 未评估近期设备活动记录。这种情况可能会发生 因为: <ph type="x-smartling-placeholder">
    </ph>
  • 设备不够可信。
  • 设备上安装的应用是 Google 无法识别的版本 开始游戏。
  • 设备存在技术问题。

账号详情字段

accountDetails 字段包含单个值,即 appLicensingVerdict, 代表应用的 Google Play 许可状态, 已登录 Google 账号。如果用户账号拥有应用的 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 保护机制,以及 它发现了已知的恶意软件

如果您已选择启用应用访问风险判定或 Play 保护机制判定 ,那么您的 API 响应将包含 environmentDetails 字段。environmentDetails 字段可以包含两个 值:appAccessRiskVerdictplayProtectVerdict

应用访问风险判定结果(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_INSTALLEDUNKNOWN_INSTALLED
表明有与相应安装来源匹配的已安装应用。
KNOWN_CAPTURINGUNKNOWN_CAPTURING
某些正在运行的应用启用了相应权限,可用于 在您的应用运行时查看屏幕。其中不包括任何已验证 设备上运行的 Google Play 已知的无障碍服务。
KNOWN_CONTROLLINGUNKNOWN_CONTROLLING
某些正在运行的应用启用了相应权限,可用于 控制设备并直接控制输入到您的应用中的内容, 用于捕获应用的输入和输出。其中不包括任何已验证 设备上运行的 Google Play 已知的无障碍服务。
KNOWN_OVERLAYSUNKNOWN_OVERLAYS
某些正在运行的应用启用了相应权限,可用于 叠加层。这会排除任何经过验证的无障碍功能 设备上运行的 Google Play 已知的服务。
EMPTY(空白值)

如果未达成必要条件,系统便不会评估应用访问风险。在 在本例中,appAccessRiskVerdict 字段为空。对于这种情况, 有多种原因,包括:

  • 设备不够可信。
  • 设备外形规格不是手机、平板电脑或可折叠设备。
  • 设备未搭载 Android 6(API 级别 23)或更高版本。
  • 设备上安装的应用是 Google Play 未知的版本。
  • 设备上的 Google Play 商店版本已过时。
  • 仅限游戏:用户账号不具备相应游戏的 Play 许可。
  • 标准请求与 verdictOptOut 参数一起使用。
  • 标准请求已与 Play Integrity API 库版本搭配使用 尚不支持针对标准请求的应用访问风险信号。

应用访问风险信号会自动排除已验证且符合以下条件的无障碍服务 已通过增强的 Google Play 无障碍功能审核(安装 设备上的任何应用商店)。“已排除”即经过验证的可访问性 因此在设备上运行时将不会返回捕获、控制或 叠加层响应的应用访问风险判定结果。请求增强的 Google 针对您的无障碍应用进行 Play 无障碍功能审核,在 Google 上发布该应用 在 Play 中确保将 isAccessibilityTool 标志设置为 true 或申请审核

下表列举了一些示例来说明判定结果及其含义( 表并未列出所有可能的结果):

应用访问风险判定结果响应示例 解读
appsDetected:
["KNOWN_INSTALLED"]
只有 Google Play 可识别的已安装应用或设备制造商在系统分区中预加载的应用。
没有任何正在运行的应用会导致捕获、控制或叠加层判定结果。
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 保护机制警告采取相应措施。如果用户无法完成 则可以阻止其执行服务器操作