本頁面說明如何解讀及使用系統傳回的完整性判定結果。無論您是提出標準要求還是傳統 API 要求,完整性判定結果都會以同一形式傳回相似的內容。透過這份判定結果,您可以瞭解裝置、應用程式和帳戶的有效性資訊。如果判定結果經過解密及驗證,應用程式的伺服器便可使用其中產生的酬載,決定如何以最佳方式處理應用程式中的特定操作或要求。
傳回的完整性判定結果格式
酬載是純文字格式的 JSON,其中包含完整性信號和開發人員提供的資訊。
一般酬載的結構如下:
{ requestDetails: { ... } appIntegrity: { ... } deviceIntegrity: { ... } accountDetails: { ... } environmentDetails: { ... } }
您必須先檢查 requestDetails
欄位中的值是否與原始要求的值相符,再逐一檢查完整性判定結果。以下章節會詳細介紹各個欄位。
要求詳情欄位
requestDetails
欄位包含要求的相關資訊,包括開發人員在標準要求的 requestHash
和傳統要求的 nonce
中提供的資訊
標準 API 要求:
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 requested. timestampMillis: "1675655009345" }
這些值應與原始要求的值相符。因此,請確認 requestPackageName
和 requestHash
與原始要求中傳送的值相符,藉此驗證 JSON 酬載的 requestDetails
部分,如下列程式碼片段所示:
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. ... }
傳統 API 要求:
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" }
這些值應與原始要求的值相符。因此,請確認 requestPackageName
和 nonce
與原始要求中傳送的值相符,以驗證 JSON 酬載的 requestDetails
部分,如下列程式碼片段所示:
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. ... }
應用程式完整性欄位
appIntegrity
欄位含有套件的相關資訊。
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
的值可能如下:
PLAY_RECOGNIZED
- 應用程式和憑證符合 Google Play 發行的版本。
UNRECOGNIZED_VERSION
- 憑證或套件名稱與 Google Play 記錄不符。
UNEVALUATED
- 系統無法評估應用程式完整性,原因是未符合必要條件,例如裝置可信度不足。
為確保權杖是由您建立的應用程式產生,請確認應用程式完整性是否符合預期,如下列程式碼片段所示:
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! }
您也可以手動檢查應用程式套件名稱、應用程式版本和應用程式憑證。
裝置完整性欄位
deviceIntegrity
欄位可包含單一值 deviceRecognitionVerdict
,後者包含一或多個標籤,代表裝置可強制執行應用程式完整性檢查的程度。如果裝置不符合任何標籤的條件,deviceIntegrity
欄位就會留空。
deviceIntegrity: { // "MEETS_DEVICE_INTEGRITY" is one of several possible values. deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"] }
根據預設,deviceRecognitionVerdict
可包含下列項目:
MEETS_DEVICE_INTEGRITY
- 應用程式在搭載 Google Play 服務的 Android 裝置上執行。該裝置已通過系統完整性檢查,符合 Android 相容性規定。
- 空白 (空白值)
- 執行應用程式的裝置可能遭受攻擊 (例如 API 掛鉤) 或系統遭到入侵 (例如已啟用 Root 權限);或者,應用程式不是在實體裝置 (例如未通過 Google Play 完整性檢查的模擬器) 上執行。
為確保權杖是來自可信任的裝置,請驗證 deviceRecognitionVerdict
是否符合預期,如下列程式碼片段所示:
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! }
如果測試裝置在達到完整性上出現問題,請確認原廠 ROM 已安裝完成 (例如,透過重新設定裝置的方式),且系統啟動載入程式已鎖定。您也可以在 Play 管理中心建立 Play Integrity API 測試。
條件式裝置標籤
如果應用程式要發布至 Google Play 遊戲電腦版,deviceRecognitionVerdict
也可以包含下列標籤:
MEETS_VIRTUAL_INTEGRITY
- 應用程式在搭載 Google Play 服務且支援 Android 的模擬器上執行。該模擬器已通過系統完整性檢查,符合主要的 Android 相容性規定。
可選用的裝置資訊
如果您選擇在完整性判定結果中接收其他標籤,deviceRecognitionVerdict
可能包含以下額外標籤:
MEETS_BASIC_INTEGRITY
- 執行應用程式的裝置通過了基本系統完整性檢查。該裝置可能不符合 Android 相容性規定,也可能未獲准執行 Google Play 服務。舉例來說,該裝置目前搭載的 Android 版本可能無法辨識、可能已解鎖系統啟動載入程式,或是可能尚未經過製造商認證。
MEETS_STRONG_INTEGRITY
- 應用程式在搭載 Google Play 服務的 Android 裝置上執行,而且裝置設有硬體支援的開機完整性等防護措施,足以充分確保系統完整性。該裝置已通過系統完整性檢查,符合 Android 相容性規定。
如果單一裝置符合所有標籤條件,就會在裝置完整性判定結果中傳回多個裝置標籤。
近期裝置活動
您也可以選擇接收最近的裝置活動,瞭解應用程式在過去一小時內針對特定裝置要求取得完整性權杖的次數。近期裝置活動資訊可幫助應用程式防範非預期的超活躍裝置。在該情況下,表示裝置可能正在遭受攻擊。您可以根據在一般裝置上安裝的應用程式每小時要求完整性權杖的預期次數,決定對每個近期裝置活動層級的信任程度。
如果選擇接收 recentDeviceActivity
,deviceIntegrity
欄位就會有兩個值:
deviceIntegrity: { deviceRecognitionVerdict: ["MEETS_DEVICE_INTEGRITY"] recentDeviceActivity: { // "LEVEL_2" is one of several possible values. deviceActivityLevel: "LEVEL_2" } }
deviceActivityLevel
定義會因模式而異,且可以
下列其中一個值:
最近的裝置活動層級 | 這部裝置上的標準 API 完整性權杖要求 (個別應用程式過去一小時內) | 這部裝置上的傳統 API 完整性權杖要求 (個別應用程式過去一小時內) |
---|---|---|
LEVEL_1 (最低) |
10 部以下 | 5 或少於 5 本 |
LEVEL_2 |
介於 11 到 25 之間 | 介於 6 到 10 之間 |
LEVEL_3 |
介於 26 至 50 之間 | 介於 11 到 15 之間 |
LEVEL_4 (最高) |
超過 50 種 | 超過 15 種 |
UNEVALUATED |
系統未評估近期裝置活動。
因為:
|
帳戶詳細資料欄位
accountDetails
欄位含有單一值 appLicensingVerdict
,
代表應用程式的 Google Play 授權狀態
登入裝置。如果使用者的帳戶擁有應用程式的 Play 授權,
表示他們已從 Google Play 下載或購買該影片
accountDetails: { // This field can be LICENSED, UNLICENSED, or UNEVALUATED. appLicensingVerdict: "LICENSED" }
appLicensingVerdict
可含有下列其中一個值:
LICENSED
- 使用者擁有應用程式授權。也就是說,使用者是從 Google Play 安裝或購買您的應用程式。
UNLICENSED
- 使用者沒有應用程式授權。這可能是因為使用者側載了您的應用程式,或者不是從 Google Play 取得應用程式。 您可以向使用者顯示 GET_LICENSED 對話方塊,解決這個問題。
UNEVALUATED
由於未符合必要條件,系統無法評估授權詳細資料。
這可能由許多因素造成,包括:
- 裝置的可信度不足。
- Google Play 無法辨識裝置上安裝的應用程式版本。
- 使用者未登入 Google Play。
依照下列程式碼片段所示,您可驗證 appLicensingVerdict
是否顯示預期結果,即可瞭解使用者是否擁有應用程式授權:
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! }
環境詳細資料欄位
您也可以選擇接收與環境相關的其他信號。應用程式存取權 風險會告訴您的應用程式,是否有其他執行中的應用程式可用於存取 擷取螢幕畫面、顯示重疊元素,或控制裝置。Play 安全防護 判定結果會指出裝置是否已啟用 Google Play 安全防護,以及 發現已知的惡意軟體
如果您選擇加入應用程式存取風險判定結果或 Play 安全防護判定結果
Google Play 管理中心,API 回應會包含
「environmentDetails
」欄位。environmentDetails
欄位可包含兩個
值為 appAccessRiskVerdict
和 playProtectVerdict
應用程式存取風險判定結果 (Beta 版)
啟用此信號後,Play Integrity API 酬載中的 environmentDetails
欄位就會包含新的應用程式存取風險判定結果。
{
requestDetails: { ... }
appIntegrity: { ... }
deviceIntegrity: { ... }
accountDetails: { ... }
environmentDetails: {
appAccessRiskVerdict: {
// This field contains one or more responses, for example the following.
appsDetected: ["KNOWN_INSTALLED", "UNKNOWN_INSTALLED", "UNKNOWN_CAPTURING"]
}
}
}
如果已評估應用程式存取風險,appAccessRiskVerdict
會包含欄位
appsDetected
,其中包含一或多則回應。這類回應屬於
根據偵測到的應用程式安裝來源,查看以下兩個群組:
Play 或系統應用程式:透過 Google Play 安裝或預先載入的應用程式 由裝置製造商儲存在裝置系統分區中 (以
FLAG_SYSTEM
)。 這類應用程式的回應前方會加上KNOWN_
。其他應用程式:非由 Google Play 安裝的應用程式,不含 是由裝置製造商在系統分區預先載入的應用程式。回應數 開頭的前置字串為
UNKNOWN_
。
系統可能會傳回下列回應:
KNOWN_INSTALLED
、UNKNOWN_INSTALLED
- 有已安裝的應用程式與對應的安裝來源相符。
KNOWN_CAPTURING
、UNKNOWN_CAPTURING
- 實際執行中的應用程式已啟用權限,可用於以下目的: 在應用程式執行期間查看螢幕畫面這不包括任何已驗證網站 Google Play 已知的無障礙服務。
KNOWN_CONTROLLING
、UNKNOWN_CONTROLLING
- 實際執行中的應用程式已啟用權限,可用於以下目的: 直接控制裝置及直接控管輸入到應用程式中的內容, 用於擷取應用程式的輸入和輸出內容這不包括任何已驗證網站 Google Play 已知的無障礙服務。
KNOWN_OVERLAYS
、UNKNOWN_OVERLAYS
- 實際執行中的應用程式已啟用權限,可用於以下目的: 在您的應用程式上顯示重疊圖片。這不包括任何已驗證的無障礙設定 即 Google Play 已知的服務。
- EMPTY (空白值)
如果未符合必要條件,系統不會評估應用程式存取風險。於 在這個範例中,
appAccessRiskVerdict
欄位是空白的。這在 這些因素包括:- 裝置可信度不足。
- 裝置板型規格並非手機、平板電腦或摺疊式裝置。
- 裝置搭載的不是 Android 6 (API 級別 23) 以上版本。
- Google Play 無法辨識裝置上安裝的應用程式版本。
- 裝置上的 Google Play 商店版本過舊。
- 僅限遊戲:使用者帳戶沒有遊戲的 Play 授權。
- 搭配
verdictOptOut
參數使用標準要求。 - 標準要求是搭配 Play Integrity API 程式庫版本使用 尚不支援標準要求的應用程式存取風險。
應用程式存取風險信號會自動排除以下類型的已驗證無障礙服務:
已通過強化的 Google Play 無障礙功能審查 (安裝自
裝置上的任何應用程式商店)。「已排除」也就是經過驗證的
在該裝置上執行的服務就不會傳回擷取、控製或
在應用程式存取風險判定結果中重疊回應。如何要求進階 Google 服務
Google Play 對您的無障礙應用程式進行無障礙功能審查,並在 Google 上發布
Google Play 確認您的應用程式已於下列位置將 isAccessibilityTool
標記設為 true:
在應用程式的資訊清單,或要求審查。
下表提供一些判定結果的示例,說明它們代表的意義 (這個 表格不會列出所有可能的結果):
應用程式存取風險判定結果的回應示例 | 解釋 |
---|---|
appsDetected: ["KNOWN_INSTALLED"]
|
只有 Google Play 可辨識的已安裝應用程式,或裝置製造商在系統分區預先載入的應用程式。 目前沒有任何執行中的應用程式會導致擷取、控製或重疊判定結果。 |
appsDetected: ["KNOWN_INSTALLED", "UNKNOWN_INSTALLED", "UNKNOWN_CAPTURING"]
|
有 Google Play 安裝的應用程式,或裝置製造商在系統分區預先載入的應用程式。 目前有其他執行中的應用程式已啟用權限,可用來查看螢幕畫面或擷取其他輸入和輸出內容。 |
appsDetected: ["KNOWN_INSTALLED", "KNOWN_CAPTURING", "UNKNOWN_INSTALLED", "UNKNOWN_CONTROLLING"]
|
當下的 Play 或系統已啟用權限,可用來查看螢幕畫面或擷取其他輸入和輸出內容。 此外,也有其他已啟用權限的應用程式正在執行,可用於控制裝置並直接控管輸入到應用程式中的內容。 |
appAccessRiskVerdict: {}
|
由於未符合必要條件,系統不會評估應用程式存取風險。舉例來說,如果裝置可信度不足,就屬這種情況。 |
視風險等級而定,您可以決定要繼續採用哪些判定結果組合,以及要對哪些判定結果採取行動。 以下程式碼片段舉例說明如何驗證 可擷取螢幕畫面或控制應用程式運作的應用程式:
Kotlin
val environmentDetails = JSONObject(payload).getJSONObject("environmentDetails") val appAccessRiskVerdict = environmentDetails.getJSONObject("appAccessRiskVerdict") if (appAccessRiskVerdict.has("appsDetected")) { val appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString() if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) { // Looks good! } }
Java
JSONObject environmentDetails = new JSONObject(payload).getJSONObject("environmentDetails"); JSONObject appAccessRiskVerdict = environmentDetails.getJSONObject("appAccessRiskVerdict"); if (appAccessRiskVerdict.has("appsDetected")) { String appsDetected = appAccessRiskVerdict.getJSONArray("appsDetected").toString() if (!appsDetected.contains("CAPTURING") && !appsDetected.contains("CONTROLLING")) { // Looks good! } }
修正應用程式存取風險判定結果
視風險等級而定,您可以決定要對哪個應用程式存取風險判定結果 是否讓使用者完成要求或動作。 您可以選擇是否要向使用者顯示 Google Play 提示 檢查應用程式存取風險判定結果。您 CLOSE_UNKNOWN_ACCESS_RISK 要求使用者關閉不明應用程式,導致 應用程式存取風險判定結果,或者您也可以顯示CLOSE_ALL_ACCESS_RISK來要求 使用者關閉所有應用程式 (已知與未知),導致應用程式存取風險判定結果。
Play 安全防護判定結果
啟用後,Play Integrity API 中的 environmentDetails
欄位
酬載會包含
Play 安全防護判定結果:
environmentDetails: {
playProtectVerdict: "NO_ISSUES"
}
playProtectVerdict
可含有下列其中一個值:
NO_ISSUES
- 已啟用 Play 安全防護功能,且未在裝置上發現任何應用程式問題。
NO_DATA
- 已啟用 Play 安全防護功能,但尚未執行掃描作業。最近可能重設過裝置或 Play 商店應用程式。
POSSIBLE_RISK
- 已停用 Play 安全防護功能。
MEDIUM_RISK
- 已啟用 Play 安全防護功能,並發現裝置上安裝了可能有害的應用程式。
HIGH_RISK
- 已啟用 Play 安全防護功能,並發現裝置上安裝了危險的應用程式。
UNEVALUATED
未評估 Play 安全防護判定結果。
這可能由許多因素造成,包括:
- 裝置可信度不足。
- 僅限遊戲:使用者帳戶沒有遊戲的 Play 授權。
Play 安全防護判定結果的使用指南
應用程式的後端伺服器可以按照風險容忍度,根據判定結果決定處置方式。以下提供一些建議和可能的使用者動作:
NO_ISSUES
- Play 安全防護功能已啟用,且未發現任何問題,因此使用者無須採取任何動作。
POSSIBLE_RISK
和NO_DATA
- 收到這些判定結果時,要求使用者檢查是否已啟用 Play 安全防護功能,且已執行掃描作業。
NO_DATA
應該只會在極少數情況下出現。 MEDIUM_RISK
和HIGH_RISK
- 視風險容忍度而定,您可以要求使用者啟動 Play 安全防護功能,並對 Play 安全防護警示採取特定動作。如果使用者無法出貨 您即可封鎖這些需求,而不被伺服器動作封鎖