Kết quả về tính toàn vẹn

Trang này mô tả cách diễn giải và xử lý kết quả về tính toàn vẹn được trả về. Cho dù bạn thực hiện yêu cầu API thông thường hay yêu cầu API kiểu cũ, kết quả về tính toàn vẹn sẽ được trả về ở cùng định dạng với nội dung tương tự. Kết quả về tính toàn vẹn sẽ cho biết thông tin về tính hợp lệ của các thiết bị, ứng dụng và tài khoản. Máy chủ của ứng dụng có thể dùng tải trọng phát sinh trong một kết quả đã được giải mã và xác minh nhằm xác định cách tốt nhất để xử lý một hành động hoặc yêu cầu cụ thể trong ứng dụng của bạn.

Định dạng của kết quả về tính toàn vẹn được trả về

Tải trọng này là tệp JSON văn bản thuần tuý chứa các tín hiệu về tính toàn vẹn cùng với thông tin do nhà phát triển cung cấp.

Tải trọng có cấu trúc tổng quát như sau:

{
  requestDetails: { ... }
  appIntegrity: { ... }
  deviceIntegrity: { ... }
  accountDetails: { ... }
  environmentDetails: { ... }
}

Trước tiên, bạn phải kiểm tra để đảm bảo rằng các giá trị trong trường requestDetails khớp với các giá trị của yêu cầu ban đầu trước khi kiểm tra từng kết quả về tính toàn vẹn. Các phần sau đây sẽ mô tả chi tiết hơn về từng trường.

Trường thông tin chi tiết về yêu cầu

Trường requestDetails chứa thông tin về yêu cầu, bao gồm thông tin do nhà phát triển cung cấp trong requestHash đối với các yêu cầu thông thường và nonce đối với các yêu cầu kiểu cũ.

Đối với các yêu cầu API thông thường:

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 prepared (computed on the server).
  timestampMillis: "1675655009345"
}

Các giá trị này phải khớp với các giá trị trong yêu cầu ban đầu. Do đó, hãy xác minh phần requestDetails trong tải trọng JSON bằng cách đảm bảo requestPackageNamerequestHash khớp với nội dung đã gửi trong yêu cầu ban đầu như được minh hoạ trong đoạn mã sau:

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

Đối với các yêu cầu API kiểu cũ:

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

Các giá trị này phải khớp với các giá trị trong yêu cầu ban đầu. Do đó, hãy xác minh phần requestDetails trong tải trọng JSON bằng cách đảm bảo requestPackageNamenonce khớp với nội dung đã gửi trong yêu cầu ban đầu như được minh hoạ trong đoạn mã sau:

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

Trường tính toàn vẹn của ứng dụng

Trường appIntegrity chứa thông tin liên quan đến gói.

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 có thể có các giá trị sau:

PLAY_RECOGNIZED
Ứng dụng và chứng chỉ khớp với các phiên bản do Google Play phân phối.
UNRECOGNIZED_VERSION
Chứng chỉ hoặc tên gói không khớp với bản ghi của Google Play.
UNEVALUATED
Tính toàn vẹn của ứng dụng chưa được đánh giá. Thiếu một yêu cầu cần thiết, chẳng hạn như thiết bị không đủ tin cậy.

Để đảm bảo rằng mã thông báo được tạo bởi một ứng dụng mà bạn tạo ra, hãy xác minh rằng tính toàn vẹn của ứng dụng đúng với dự kiến như trong đoạn mã sau:

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

Bạn cũng có thể kiểm tra tên gói, phiên bản và chứng chỉ của ứng dụng theo cách thủ công.

Trường tính toàn vẹn của thiết bị

Trường deviceIntegrity có thể chứa một giá trị deviceRecognitionVerdict duy nhất. Giá trị này có một hoặc nhiều nhãn thể hiện mức độ hiệu quả của thiết bị trong việc thực thi tính toàn vẹn của ứng dụng. Nếu thiết bị không đáp ứng tiêu chí của bất kỳ nhãn nào, thì trường deviceIntegrity sẽ trống.

deviceIntegrity: {
  // "MEETS_DEVICE_INTEGRITY" is one of several possible values.
  deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"]
}

Theo mặc định, deviceRecognitionVerdict có thể chứa các thông tin sau:

MEETS_DEVICE_INTEGRITY
Ứng dụng đang chạy trên một thiết bị Android có Dịch vụ Google Play. Thiết bị vượt qua quy trình kiểm tra tính toàn vẹn của hệ thống và đáp ứng các yêu cầu về khả năng tương thích với Android.
Trống (giá trị trống)
Ứng dụng đang chạy trên một thiết bị có dấu hiệu bị tấn công (chẳng hạn như hook API) hoặc bị xâm phạm hệ thống (chẳng hạn như đã bị can thiệp vào hệ thống) hoặc ứng dụng không chạy trên một thiết bị thực (chẳng hạn như trình mô phỏng không vượt qua được quy trình kiểm tra tính toàn vẹn của Google Play).

Để đảm bảo rằng mã thông báo đến từ một thiết bị đáng tin cậy, hãy xác minh deviceRecognitionVerdict theo dự kiến như trong đoạn mã sau:

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

Nếu bạn gặp sự cố liên quan đến tính toàn vẹn của thiết bị đang kiểm tra, hãy đảm bảo bạn đã cài đặt ROM gốc (ví dụ: bằng cách đặt lại thiết bị) và đã khoá trình tải khởi động. Bạn cũng có thể tạo phép kiểm thử API Tính toàn vẹn của Play trong Play Console.

Nhãn thiết bị có điều kiện

Nếu ứng dụng của bạn đang được phát hành trên Google Play Games dành cho máy tính, thì deviceRecognitionVerdict cũng có thể chứa nhãn sau:

MEETS_VIRTUAL_INTEGRITY
Ứng dụng đang chạy trên một trình mô phỏng Android có Dịch vụ Google Play. Trình mô phỏng này vượt qua quy trình kiểm tra tính toàn vẹn của hệ thống và đáp ứng các yêu cầu chính về khả năng tương thích với Android.

Thông tin không bắt buộc về thiết bị

Nếu bạn chọn nhận các nhãn bổ sung trong kết quả về tính toàn vẹn, thì deviceRecognitionVerdict có thể chứa các nhãn bổ sung sau đây:

MEETS_BASIC_INTEGRITY
Ứng dụng này đang chạy trên một thiết bị đã vượt qua quy trình kiểm tra cơ bản về tính toàn vẹn của hệ thống. Có thể thiết bị này không đáp ứng yêu cầu về khả năng tương thích với Android và không được phê duyệt để sử dụng dịch vụ Google Play. Ví dụ: có thể thiết bị này đang chạy một phiên bản Android không xác định, dùng trình tải khởi động đã được mở khoá hoặc chưa được nhà sản xuất chứng nhận.
MEETS_STRONG_INTEGRITY
Ứng dụng này đang chạy trên một thiết bị Android có Dịch vụ Google Play, đồng thời có sự đảm bảo chắc chắn về tính toàn vẹn của hệ thống, chẳng hạn như bằng chứng dựa trên phần cứng về tính toàn vẹn khi khởi động. Thiết bị vượt qua quy trình kiểm tra tính toàn vẹn của hệ thống và đáp ứng các yêu cầu về khả năng tương thích với Android.

Một thiết bị sẽ trả về nhiều nhãn thiết bị trong kết quả về tính toàn vẹn của thiết bị nếu từng tiêu chí của nhãn được đáp ứng.

Hoạt động gần đây trên thiết bị (beta)

Bạn cũng có thể chọn sử dụng hoạt động gần đây trên thiết bị. Hoạt động này cho biết số lần ứng dụng của bạn đã yêu cầu một mã thông báo về tính toàn vẹn trên một thiết bị cụ thể trong 1 giờ qua. Bạn có thể sử dụng hoạt động gần đây trên thiết bị để bảo vệ ứng dụng khỏi các thiết bị không mong muốn, hoạt động quá mức. Đây có thể là dấu hiệu của một cuộc tấn công đang diễn ra. Bạn có thể quyết định mức độ tin cậy cho mỗi cấp hoạt động gần đây trên thiết bị, dựa trên số lần bạn dự kiến ứng dụng của mình được cài đặt trên một thiết bị thông thường để yêu cầu một mã thông báo về tính toàn vẹn mỗi giờ.

Nếu bạn chọn nhận recentDeviceActivity, trường deviceIntegrity sẽ có 2 giá trị:

deviceIntegrity: {
  deviceRecognitionVerdict: "MEETS_DEVICE_INTEGRITY"
  recentDeviceActivity: "LEVEL_2"
}

recentDeviceActivity có thể có một trong những giá trị sau đây:

LEVEL_1
Cấp thấp nhất: Ứng dụng đã yêu cầu tối đa 10 mã thông báo về tính toàn vẹn trên thiết bị này trong 1 giờ qua.
LEVEL_2
Ứng dụng đã yêu cầu từ 11 đến 25 mã thông báo về tính toàn vẹn trên thiết bị này trong 1 giờ qua.
LEVEL_3
Ứng dụng đã yêu cầu từ 26 đến 50 mã thông báo về tính toàn vẹn trên thiết bị này trong 1 giờ qua.
LEVEL_4
Cấp cao nhất: Ứng dụng yêu cầu hơn 50 mã thông báo về tính toàn vẹn trên thiết bị này trong 1 giờ qua.
UNEVALUATED

Hoạt động gần đây trên thiết bị chưa được đánh giá. Điều này có thể xảy ra vì một vài lý do như sau:

  • Thiết bị không đủ tin cậy.
  • Google Play không xác định được phiên bản ứng dụng của bạn đã cài đặt trên thiết bị.
  • Các vấn đề kỹ thuật trên thiết bị.

Các định nghĩa về cấp độ chỉ mang tính tương đối và có thể thay đổi.

Trường thông tin chi tiết về tài khoản

Trường accountDetails chứa một giá trị appLicensingVerdict duy nhất, đại diện cho trạng thái cấp phép/quyền của ứng dụng.

accountDetails: {
  // This field can be LICENSED, UNLICENSED, or UNEVALUATED.
  appLicensingVerdict: "LICENSED"
}

appLicensingVerdict có thể có một trong các giá trị sau:

LICENSED
Người dùng có quyền sử dụng ứng dụng. Nói cách khác, người dùng đã cài đặt hoặc mua ứng dụng của bạn trên Google Play.
UNLICENSED
Người dùng không có quyền sử dụng ứng dụng. Chẳng hạn như khi người dùng cài đặt ứng dụng của bạn không qua cửa hàng ứng dụng hoặc có được ứng dụng thông qua một bên khác không phải là Google Play. Bạn có thể cho người dùng thấy hộp thoại GET_LICENSED để khắc phục vấn đề này.
UNEVALUATED

Thông tin cấp phép/quyền chưa được đánh giá vì thiếu một yêu cầu cần thiết.

Điều này có thể xảy ra vì một vài lý do như sau:

  • Thiết bị không đủ tin cậy.
  • Google Play không xác định được phiên bản ứng dụng của bạn đã cài đặt trên thiết bị.
  • Người dùng chưa đăng nhập vào Google Play.

Để kiểm tra xem người dùng có quyền sử dụng ứng dụng của bạn hay không, hãy xác minh rằng appLicensingVerdict là đúng với dự kiến như trong đoạn mã sau:

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

Trường thông tin chi tiết về môi trường

Nếu bạn đã chọn sử dụng kết quả kiểm tra Play Protect trong Google Play Console, thì phản hồi của API sẽ bao gồm trường environmentDetails. Trường environmentDetails chứa một giá trị playProtectVerdict duy nhất. Giá trị này cung cấp thông tin về Google Play Protect trên thiết bị.

environmentDetails: {
  playProtectVerdict: "NO_ISSUES"
}

playProtectVerdict có thể có một trong những giá trị sau:

NO_ISSUES
Play Protect đã bật và không phát hiện thấy bất kỳ vấn đề nào về ứng dụng trên thiết bị.
NO_DATA
Play Protect đã bật nhưng chưa quét. Thiết bị hoặc ứng dụng Cửa hàng Play gần đây có thể đã được đặt lại.
POSSIBLE_RISK
Play Protect đã bị tắt.
MEDIUM_RISK
Play Protect đã bật và phát hiện thấy các ứng dụng có khả năng gây hại được cài đặt trên thiết bị này.
HIGH_RISK
Play Protect đã bật và phát hiện thấy các ứng dụng nguy hiểm được cài đặt trên thiết bị này.
UNEVALUATED

Kết quả kiểm tra Play Protect chưa được đánh giá.

Điều này có thể xảy ra vì một vài lý do như sau:

  • Thiết bị không đủ tin cậy.
  • Chỉ dành cho trò chơi: Tài khoản người dùng không phải là LICENSED.

Hướng dẫn cách sử dụng kết quả kiểm tra Play Protect

Máy chủ phụ trợ của ứng dụng có thể quyết định cách hành động dựa theo kết quả và khả năng chấp nhận rủi ro của bạn. Sau đây là một số đề xuất và hành động mà người dùng có thể thực hiện:

NO_ISSUES
Play Protect đang bật và không phát hiện thấy vấn đề nên người dùng không cần thực hiện hành động nào.
POSSIBLE_RISKNO_DATA
Khi nhận được những kết quả này, hãy yêu cầu người dùng kiểm tra để đảm bảo rằng Play Protect đang bật và đã thực hiện quy trình quét. NO_DATA chỉ nên xuất hiện trong một số trường hợp hiếm gặp.
MEDIUM_RISKHIGH_RISK
Tuỳ thuộc vào khả năng chấp nhận rủi ro, bạn có thể yêu cầu người dùng chạy Play Protect và xử lý các cảnh báo của Play Protect. Nếu người dùng không thể thực hiện các yêu cầu này, bạn có thể chặn họ thao tác trên máy chủ.