发起标准 API 请求

此页面介绍了如何对完整性结果发起标准 API 请求,Android 5.0(API 级别 21)或更高版本支持这些请求。每当您的应用进行服务器调用以检查交互是否真实时,您都可以对完整性结果发起标准 API 请求。

概述

Sequence diagram that shows the high-level design of the Play Integrity
API

标准请求包含两个部分

  • 准备完整性令牌提供程序(一次性):您需要在需要获取完整性结果之前很久就调用完整性 API 来准备完整性令牌提供程序。例如,您可以在应用启动时或在需要完整性结果之前的后台执行此操作。
  • 请求完整性令牌(按需):每当您的应用发出您想要检查其真实性的服务器请求时,您都会请求一个完整性令牌并将其发送到您的应用后端服务器以进行解密和验证。然后您的后端服务器可以决定如何操作。

准备完整性令牌提供程序(一次性)

  1. 您的应用使用您的 Google Cloud 项目编号调用完整性令牌提供程序。
  2. 您的应用将完整性令牌提供程序保存在内存中,以便进一步进行证明检查调用。

请求完整性令牌(按需)

  1. 对于需要保护的用户操作,您的应用会计算要发出的请求的哈希值(使用任何合适的哈希算法,例如 SHA256)。
  2. 您的应用请求完整性令牌,并传递请求哈希值。
  3. 您的应用从 Play 完整性 API 接收已签名和加密的完整性令牌。
  4. 您的应用将完整性令牌传递到您的应用后端。
  5. 您的应用后端将令牌发送到 Google Play 服务器。Google Play 服务器会解密并验证结果,并将结果返回到您的应用后端。
  6. 您的应用后端根据令牌有效负载中包含的信号决定如何继续。
  7. 您的应用后端将决策结果发送到您的应用。

准备完整性令牌提供程序(一次性)

在从 Google Play 发起完整性结果的标准请求之前,您必须准备(或“预热”)完整性令牌提供程序。这允许 Google Play 智能地在设备上缓存部分证明信息,以便在您请求完整性结果时减少关键路径上的延迟。再次准备令牌提供程序是一种重复较少资源密集型完整性检查的方法,这将使您请求的下一个完整性结果更加最新。

您可能需要准备完整性令牌提供程序

  • 当您的应用启动时(即冷启动)。准备令牌提供程序是异步的,因此不会影响启动时间。如果您计划在应用启动后不久发出完整性结果请求(例如,当用户登录或玩家加入游戏时),此选项将非常有效。
  • 当您的应用打开时(即热启动)。但是,请注意,每个应用实例每分钟只能准备最多 5 次完整性令牌。
  • 在您想在完整性结果请求之前预先准备令牌的任何后台时间。

要准备完整性令牌提供程序,请执行以下操作

  1. 创建一个 StandardIntegrityManager,如下例所示。
  2. 构建一个 PrepareIntegrityTokenRequest,通过 setCloudProjectNumber() 方法提供 Google Cloud 项目编号。
  3. 使用管理器调用 prepareIntegrityToken(),并提供 PrepareIntegrityTokenRequest

Java

import com.google.android.gms.tasks.Task;

// Create an instance of a manager.
StandardIntegrityManager standardIntegrityManager =
    IntegrityManagerFactory.createStandard(applicationContext);

StandardIntegrityTokenProvider integrityTokenProvider;
long cloudProjectNumber = ...;

// Prepare integrity token. Can be called once in a while to keep internal
// state fresh.
standardIntegrityManager.prepareIntegrityToken(
    PrepareIntegrityTokenRequest.builder()
        .setCloudProjectNumber(cloudProjectNumber)
        .build())
    .addOnSuccessListener(tokenProvider -> {
        integrityTokenProvider = tokenProvider;
    })
    .addOnFailureListener(exception -> handleError(exception));

Unity

IEnumerator PrepareIntegrityTokenCoroutine() {
    long cloudProjectNumber = ...;

    // Create an instance of a standard integrity manager.
    var standardIntegrityManager = new StandardIntegrityManager();

    // Request the token provider.
    var integrityTokenProviderOperation =
      standardIntegrityManager.PrepareIntegrityToken(
        new PrepareIntegrityTokenRequest(cloudProjectNumber));

    // Wait for PlayAsyncOperation to complete.
    yield return integrityTokenProviderOperation;

    // Check the resulting error code.
    if (integrityTokenProviderOperation.Error != StandardIntegrityErrorCode.NoError)
    {
        AppendStatusLog("StandardIntegrityAsyncOperation failed with error: " +
                integrityTokenProviderOperation.Error);
        yield break;
    }

    // Get the response.
    var integrityTokenProvider = integrityTokenProviderOperation.GetResult();
}

原生

/// Initialize StandardIntegrityManager
StandardIntegrityManager_init(/* app's java vm */, /* an android context */);
/// Create a PrepareIntegrityTokenRequest opaque object.
int64_t cloudProjectNumber = ...;
PrepareIntegrityTokenRequest* tokenProviderRequest;
PrepareIntegrityTokenRequest_create(&tokenProviderRequest);
PrepareIntegrityTokenRequest_setCloudProjectNumber(tokenProviderRequest, cloudProjectNumber);

/// Prepare a StandardIntegrityTokenProvider opaque type pointer and call
/// StandardIntegrityManager_prepareIntegrityToken().
StandardIntegrityTokenProvider* tokenProvider;
StandardIntegrityErrorCode error_code =
        StandardIntegrityManager_prepareIntegrityToken(tokenProviderRequest, &tokenProvider);

/// ...
/// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
if (error_code != STANDARD_INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.

IntegrityResponseStatus token_provider_status;

/// Check for error codes.
StandardIntegrityErrorCode error_code =
        StandardIntegrityTokenProvider_getStatus(tokenProvider, &token_provider_status);
if (error_code == STANDARD_INTEGRITY_NO_ERROR
    && token_provider_status == INTEGRITY_RESPONSE_COMPLETED)
{
    /// continue to request token from the token provider
}
/// ...
/// Remember to free up resources.
PrepareIntegrityTokenRequest_destroy(tokenProviderRequest);

保护请求免受篡改(推荐)

当您使用 Play Integrity API 检查应用中的用户操作时,您可以利用 requestHash 字段来减轻篡改攻击的影响。例如,游戏可能希望将玩家的分数报告给游戏的后端服务器,而您的服务器需要确保此分数未被代理服务器篡改。Play Integrity API 会在签名的完整性响应中返回您在 requestHash 字段中设置的值。如果没有 requestHash,完整性令牌将仅绑定到设备,而不是特定请求,这会增加攻击的可能性。以下说明描述了如何有效地使用 requestHash 字段。

当您请求完整性验证时

  • 计算所有相关请求参数的摘要(例如,稳定请求序列化的 SHA256)。requestHash 字段中设置的值最大长度为 500 字节。将任何对您正在检查或保护的操作至关重要或相关的应用请求数据包含在 requestHash 中。requestHash 字段会原样包含在完整性令牌中,因此较长的值可能会增加请求大小。
  • 将摘要作为 requestHash 字段提供给 Play Integrity API,并获取完整性令牌。

当您收到完整性验证结果时

  • 解码完整性令牌,并提取 requestHash 字段。
  • 以与应用中相同的方式计算请求的摘要(例如,稳定请求序列化的 SHA256)。
  • 比较应用端和服务器端的摘要。如果它们不匹配,则请求不可靠。

请求完整性验证(按需)

准备完整性令牌提供程序后,您可以开始向 Google Play 请求完整性验证。为此,请完成以下步骤

  1. 获取 StandardIntegrityTokenProvider,如上所示。
  2. 构造一个 StandardIntegrityTokenRequest,通过 setRequestHash 方法提供要保护的用户操作的请求哈希。
  3. 使用完整性令牌提供程序调用 request(),提供 StandardIntegrityTokenRequest

Java

import com.google.android.gms.tasks.Task;

StandardIntegrityTokenProvider integrityTokenProvider;

// See above how to prepare integrityTokenProvider.

// Request integrity token by providing a user action request hash. Can be called
// several times for different user actions.
String requestHash = "2cp24z...";
Task<StandardIntegrityToken> integrityTokenResponse =
    integrityTokenProvider.request(
        StandardIntegrityTokenRequest.builder()
            .setRequestHash(requestHash)
            .build());
integrityTokenResponse
    .addOnSuccessListener(response -> sendToServer(response.token()))
    .addOnFailureListener(exception -> handleError(exception));

Unity

IEnumerator RequestIntegrityTokenCoroutine() {
    StandardIntegrityTokenProvider integrityTokenProvider;

    // See above how to prepare integrityTokenProvider.

    // Request integrity token by providing a user action request hash. Can be called
    // several times for different user actions.
    String requestHash = "2cp24z...";
    var integrityTokenOperation = integrityTokenProvider.Request(
      new StandardIntegrityTokenRequest(requestHash)
    );

    // Wait for PlayAsyncOperation to complete.
    yield return integrityTokenOperation;

    // Check the resulting error code.
    if (integrityTokenOperation.Error != StandardIntegrityErrorCode.NoError)
    {
        AppendStatusLog("StandardIntegrityAsyncOperation failed with error: " +
                integrityTokenOperation.Error);
        yield break;
    }

    // Get the response.
    var integrityToken = integrityTokenOperation.GetResult();
}

原生

/// Create a StandardIntegrityTokenRequest opaque object.
const char* requestHash = ...;
StandardIntegrityTokenRequest* tokenRequest;
StandardIntegrityTokenRequest_create(&tokenRequest);
StandardIntegrityTokenRequest_setRequestHash(tokenRequest, requestHash);

/// Prepare a StandardIntegrityToken opaque type pointer and call
/// StandardIntegrityTokenProvider_request(). Can be called several times for
/// different user actions. See above how to prepare token provider.
StandardIntegrityToken* token;
StandardIntegrityErrorCode error_code =
        StandardIntegrityTokenProvider_request(tokenProvider, tokenRequest, &token);

/// ...
/// Proceed to polling iff error_code == STANDARD_INTEGRITY_NO_ERROR
if (error_code != STANDARD_INTEGRITY_NO_ERROR)
{
    /// Remember to call the *_destroy() functions.
    return;
}
/// ...
/// Use polling to wait for the async operation to complete.

IntegrityResponseStatus token_status;

/// Check for error codes.
StandardIntegrityErrorCode error_code =
        StandardIntegrityToken_getStatus(token, &token_status);
if (error_code == STANDARD_INTEGRITY_NO_ERROR
    && token_status == INTEGRITY_RESPONSE_COMPLETED)
{
    const char* integrityToken = StandardIntegrityToken_getToken(token);
}
/// ...
/// Remember to free up resources.
StandardIntegrityTokenRequest_destroy(tokenRequest);
StandardIntegrityToken_destroy(token);
StandardIntegrityTokenProvider_destroy(tokenProvider);
StandardIntegrityManager_destroy();

解密并验证完整性验证结果

请求完整性验证后,Play Integrity API 会提供一个加密的响应令牌。要获取设备完整性验证结果,您必须在 Google 的服务器上解密完整性令牌。为此,请完成以下步骤

  1. 在与您的应用关联的 Google Cloud 项目中创建一个服务帐号
  2. 在您的应用服务器上,使用 playintegrity 作用域从您的服务帐号凭据中获取访问令牌,并发出以下请求

    playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken -d \
    '{ "integrity_token": "INTEGRITY_TOKEN" }'
  3. 读取 JSON 响应。

生成的有效负载是一个包含完整性验证结果的纯文本令牌。

自动重放保护

为了减轻重放攻击,Google Play 会自动确保每个完整性令牌不能重复使用多次。尝试重复解密同一个令牌将导致空验证结果。