完整性验证

此页面介绍如何解释和处理返回的完整性验证结果。无论您发出标准 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"
}

这些值应与原始请求的值匹配。因此,请通过确保 requestPackageNamerequestHash 与原始请求中发送的值匹配来验证 JSON 有效负载的 requestDetails 部分,如下面的代码片段所示

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"
}

这些值应与原始请求的值匹配。因此,请通过确保 requestPackageNamenonce 与原始请求中发送的值匹配来验证 JSON 有效负载的 requestDetails 部分,如下面的代码片段所示

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 Console 中创建 Play 完整性 API 测试

条件设备标签

如果您的应用发布到适用于 PC 的 Google Play 游戏,则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 次或更少 5 次或更少
LEVEL_2 11 到 25 次之间 6 到 10 次之间
LEVEL_3 26 到 50 次之间 11 到 15 次之间
LEVEL_4(最高) 超过 50 次 超过 15 次
UNEVALUATED 未评估最近的设备活动。这可能是因为
  • 设备不够可靠。
  • 安装在设备上的应用版本在 Google Play 中未知。
  • 设备上的技术问题。

帐户详细信息字段

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 Protect 判定会告诉您设备上是否启用了 Google Play Protect 以及它是否已发现已知的恶意软件。

如果您已在 Google Play Console 中选择加入应用访问风险判定或 Play Protect 判定,则您的 API 响应将包含environmentDetails字段。environmentDetails字段可以包含两个值,appAccessRiskVerdictplayProtectVerdict

应用访问风险判定(测试版)

启用后,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 Protect 判决

启用后,Play Integrity API 负载 中的 environmentDetails 字段将包含 Play Protect 判决。

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

playProtectVerdict 可以具有以下值之一

NO_ISSUES
Play Protect 已开启,并且未在设备上发现任何应用问题。
NO_DATA
Play Protect 已开启,但尚未执行扫描。设备或 Play 商店应用可能已最近重置。
POSSIBLE_RISK
Play Protect 已关闭。
MEDIUM_RISK
Play Protect 已开启,并且已发现设备上安装了潜在有害的应用。
HIGH_RISK
Play Protect 已开启,并且已发现设备上安装了危险的应用。
UNEVALUATED

未评估 Play Protect 判决。

这可能出于多种原因,包括以下原因

  • 设备不够可靠。
  • 仅限游戏:用户帐户没有游戏的 Play 许可证。

有关使用 Play Protect 判决的指南

您的应用后端服务器可以根据您的风险承受能力,决定如何根据判决采取行动。以下是一些建议和潜在的用户操作

NO_ISSUES
Play Protect 已开启且未发现任何问题,因此无需用户操作。
POSSIBLE_RISKNO_DATA
收到这些判决时,请要求用户检查 Play Protect 是否已开启并已执行扫描。NO_DATA 仅在极少数情况下才会出现。
MEDIUM_RISKHIGH_RISK
根据您的风险承受能力,您可以要求用户启动 Play Protect 并对 Play Protect 警告采取行动。如果用户无法满足这些要求,您可以阻止他们执行服务器操作。