发起标准 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 会自动确保每个完整性令牌不能重复使用多次。尝试重复解密相同的令牌将导致空验证。