发出标准 API 请求

本页面介绍如何发出标准 API 请求来获取完整性判定,该功能在 Android 5.0 (API 级别 21) 及更高版本上受支持。每当您的应用发出服务器调用以检查互动是否真实时,您都可以发出标准 API 请求来获取完整性判定。

概览

Sequence diagram that shows the high-level design of the 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();
}

Unreal Engine

// .h
void MyClass::OnPrepareIntegrityTokenCompleted(
  EStandardIntegrityErrorCode ErrorCode,
  UStandardIntegrityTokenProvider* Provider)
{
  // Check the resulting error code.
  if (ErrorCode == EStandardIntegrityErrorCode::StandardIntegrity_NO_ERROR)
  {
    // ...
  }
}

// .cpp
void MyClass::PrepareIntegrityToken()
{
  int64 CloudProjectNumber = ...

  // Create the Integrity Token Request.
  FPrepareIntegrityTokenRequest Request = { CloudProjectNumber };

  // Create a delegate to bind the callback function.
  FPrepareIntegrityOperationCompletedDelegate Delegate;

  // Bind the completion handler (OnPrepareIntegrityTokenCompleted) to the delegate.
  Delegate.BindDynamic(this, &MyClass::OnPrepareIntegrityTokenCompleted);

  // Initiate the prepare integrity token operation, passing the delegate to handle the result.
  GetGameInstance()
    ->GetSubsystem<UStandardIntegrityManager>()
    ->PrepareIntegrityToken(Request, Delegate);
}

原生

/// 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();
}

Unreal Engine

// .h
void MyClass::OnRequestIntegrityTokenCompleted(
  EStandardIntegrityErrorCode ErrorCode,
  UStandardIntegrityToken* Response)
{
  // Check the resulting error code.
  if (ErrorCode == EStandardIntegrityErrorCode::StandardIntegrity_NO_ERROR)
  {
    // Get the token.
    FString Token = Response->Token;
  }
}

// .cpp
void MyClass::RequestIntegrityToken()
{
  UStandardIntegrityTokenProvider* Provider = ...

  // Prepare the UStandardIntegrityTokenProvider.

  // Request integrity token by providing a user action request hash. Can be called
  // several times for different user actions.
  FString RequestHash = ...;
  FStandardIntegrityTokenRequest Request = { RequestHash };

  // Create a delegate to bind the callback function.
  FStandardIntegrityOperationCompletedDelegate Delegate;

  // Bind the completion handler (OnRequestIntegrityTokenCompleted) to the delegate.
  Delegate.BindDynamic(this, &MyClass::OnRequestIntegrityTokenCompleted);

  // Initiate the standard integrity token request, passing the delegate to handle the result.
  Provider->Request(Request, Delegate);
}

原生

/// 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 会自动确保每个完整性令牌无法多次重复使用。尝试重复解密同一令牌会导致判决被清除。对于已启用重放保护的令牌,解码后的判决会返回如下结果:

  • 设备识别判定将为空。
  • 应用识别判定和应用许可判定将设为 UNEVALUATED
  • 使用 Play 管理中心启用的任何可选判定都将设为 UNEVALUATED(如果是多值判定,则设为空判定)。