发出标准 API 请求

本页介绍了如何发出旨在获取完整性判定结果的标准 API 请求(Android 5.0 [API 级别 21] 或更高版本均支持该功能)。每当您的应用进行服务器调用时,您都可以发出标准 API 请求以获取完整性判定结果,从而检查互动是否真实。

概览

概要介绍 Play Integrity API 设计的序列图

标准请求由两部分组成:

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

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

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

请求完整性令牌(按需):

  1. 对于需要保护的用户操作,您的应用会计算要发出的请求的哈希值(使用任何合适的哈希算法,例如 SHA256)。
  2. 应用请求完整性令牌,并传递请求哈希值。
  3. 应用从 Play Integrity 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 项目中创建一个服务账号。在账号创建过程中,您需要向服务账号授予 Service Account UserService Usage Consumer 角色。
  2. 在应用服务器上,使用 playintegrity 范围从服务账号凭据中提取访问令牌,然后发出以下请求:

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

生成的载荷是包含完整性判定的纯文本令牌。

自动防范重放攻击

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