版本相容性
Credential Manager 的還原憑證功能適用於搭載 Android 9 (API 級別 28) 以上版本、Google Play 服務 (GMS) 核心版本 242200000 (v24.22) 以上版本,以及 androidx.credentials 程式庫 1.5.0 以上版本的裝置。在沒有 Google Play 服務或搭載舊版 GMS 的裝置上,請擷取 CreateCredentialProviderConfigurationException 和 GetCredentialProviderConfigurationException,讓應用程式順利改用標準登入方式。
必要條件
設定 WebAuthn 依賴方伺服器,類似於密碼金鑰的伺服器。如果您已設定伺服器來處理密碼金鑰驗證,可以重複使用相同的 WebAuthn 註冊和宣告端點來還原金鑰。
依附元件
在應用程式模組的 build.gradle 檔案中新增下列依附元件:
Kotlin
dependencies { implementation("androidx.credentials:credentials:1.7.0-alpha02") implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02") }
Groovy
dependencies { implementation "androidx.credentials:credentials:1.7.0-alpha02" implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02" }
還原憑證適用於 androidx.credentials 程式庫 1.5.0 以上版本。不過,建議您盡可能使用最新穩定版本的依附元件。
android:allowBackup 對還原憑證的影響
android:allowBackup 資訊清單屬性可控制應用程式資料備份,並透過三種不同方式與 Restore Credentials 互動:
- 還原金鑰轉移 (與
allowBackup無關):無論android:allowBackup是true還是false,Google Play 服務都會使用端對端加密雲端備份或本機裝置間 (D2D) 轉移,轉移RestoreCredential金鑰組。如果是 D2D 轉移,還原金鑰一律會轉移;只有在使用者透過 Android 設定停用雲端備份時,才不會轉移。 - 在
BackupAgent中進行背景還原 (需要android:allowBackup="true"):只有在資訊清單中設定android:allowBackup="true"和android:backupAgent時,系統才會在使用者於新裝置上開啟應用程式 (BackupAgent.onRestoreFinished()) 之前,在背景自動登入。如果為android:allowBackup="false",請在使用者首次啟動主要Activity時還原工作階段。您也可以同時採用這兩種方法,讓還原作業更穩健。 - Android Studio 雲端備份測試 (需要
android:allowBackup="true"):在 Android Studio 的備份與還原工具中測試雲端備份類型時,需要android:allowBackup="true",但裝置對裝置測試則不受allowBackup影響。
總覽
還原憑證在使用者的新舊裝置上,會經歷三個階段的生命週期:
- 在舊裝置上建立還原金鑰:使用者登入後 (或在應用程式啟動時,如果使用者已登入,且本機儲存空間中未記錄還原金鑰),請從依賴方伺服器擷取建立選項,並呼叫
credentialManager.createCredential()。 - 在新裝置上使用還原金鑰登入:使用雙層策略擷取轉移的
RestoreCredential:首先,在BackupAgent.onRestoreFinished()中自動執行 (當android:allowBackup="true"時),以便立即恢復背景同步和推播通知;其次,在首次啟動應用程式時,在啟動器Activity.onCreate()中做為備用方案。 - 在登出時刪除還原金鑰:當使用者登出或工作階段遭伺服器撤銷時,請使用
ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL)呼叫credentialManager.clearCredentialState(),並清除本機同步旗標。
建立還原金鑰
應用程式應涵蓋使用者登入的所有情況,確保活躍使用者已建立還原金鑰。在下列情況下建立還原金鑰:
- 如果使用者已登入,且尚未建立還原金鑰 (例如在主要
Activity的onCreate()方法中)。 - 使用者登入或完成新帳戶註冊流程時。
為避免每次啟動應用程式時都呼叫 createCredential(),請在本機儲存空間中儲存本機同步旗標,例如 has_synced_restore_credential。請勿將這個旗標儲存在預設備份的 SharedPreferences 或 DataStore 中,除非該檔案已在 data_extraction_rules.xml 中排除;否則,自動備份會將 has_synced_restore_credential = true 複製到新裝置上,並防止新裝置產生或輪替自己的還原金鑰。
例項化 Credential Manager
使用應用程式的活動背景資訊,將 CredentialManager 物件例項化。
// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)
從應用程式伺服器取得憑證建立選項
在應用程式伺服器中使用符合 FIDO 規範的程式庫,將建立還原憑證所需的 PublicKeyCredentialCreationOptions JSON 傳送至用戶端應用程式。如要進一步瞭解伺服器端導入作業,請參閱伺服器端指南。
建立選項 JSON 的結構與密碼金鑰註冊相同:
{
"challenge": "<base64url-encoded challenge>",
"rp": {
"name": "<relying party name>",
"id": "<relying party host name>"
},
"user": {
"id": "<base64url-encoded user ID>",
"name": "<user name>",
"displayName": "<user display name>"
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
}
],
"attestation": "none",
"excludeCredentials": [
{
"id": "<base64url-encoded credential ID to exclude>",
"type": "public-key"
}
],
"authenticatorSelection": {
"requireResidentKey": true,
"residentKey": "required",
"userVerification": "required"
}
}
請確認建立選項 JSON 符合下列規定:
challenge和user.id:將這兩個值編碼為未填補的 Base64URL 字串 (將+替換為-、/替換為_,並移除結尾的=)。標準 Base64 字元或原始整數 ID 會導致CreateRestoreCredentialDomException或IllegalArgumentException。rp.id:設為依賴方的網域 (例如example.com)。pubKeyCredParams:包含{"type": "public-key", "alg": -7}(ES256) 和{"type": "public-key", "alg": -257}(RS256)。
建立還原金鑰
從伺服器收到公開金鑰建立選項 JSON 後,請將 JSON 包裝在 CreateRestoreCredentialRequest 物件中,並在 CredentialManager 物件上呼叫 createCredential()。
// requestJson contains the credential creation options sent by the server
val createRestoreRequest = CreateRestoreCredentialRequest(requestJson)
val response = try {
credentialManager.createCredential(context, createRestoreRequest)
} catch (e: E2eeUnavailableException) {
// Fall back to local device-to-device (D2D) restore if cloud E2EE is unavailable
credentialManager.createCredential(
context,
CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false),
)
}
程式碼重點
CreateRestoreCredentialRequest物件包含下列欄位:requestJson:應用程式伺服器以 Web Authentication API 格式傳送的憑證建立選項,適用於PublicKeyCredentialCreationOptionsJSON。isCloudBackupEnabled:Boolean欄位,用於判斷是否應將還原金鑰備份到雲端。根據預設,這個旗標為true。這個欄位的值如下:true:(建議) 如果使用者已啟用 Google 備份和端對端加密 (例如螢幕鎖定),這個值會啟用還原金鑰備份至雲端的功能。false:這個值會將金鑰儲存在本機,而非雲端。如果使用者選擇從雲端還原,新裝置上就不會有該金鑰,但金鑰仍會在透過本機傳輸線或 Wi-Fi 進行裝置對裝置 (D2D) 設定時轉移。
處理憑證建立回應
Credential Manager API 會傳回 CreateRestoreCredentialResponse 類型的回應。這項回應包含 JSON 格式的公開金鑰憑證註冊回應。
將 response.registrationResponseJson 從應用程式傳送至依賴方伺服器,驗證並儲存公開金鑰和使用者帳戶,然後將本機同步標記記錄在本機儲存空間中。如要進一步瞭解伺服器端驗證,請參閱密碼金鑰指南。
在還原金鑰建立程序中,請處理下列例外狀況:
CreateRestoreCredentialDomException:如果requestJson無效,且不符合PublicKeyCredentialCreationOptionsJSON的 WebAuthn 格式,就會發生這個例外狀況。E2eeUnavailableException:如果isCloudBackupEnabled為true,但使用者的裝置沒有資料備份或端對端加密 (例如螢幕鎖定),就會發生這個例外狀況。
為確保即使雲端 E2EE 無法使用,也能為本機裝置對裝置 (D2D) 轉移作業建立還原憑證,請擷取E2eeUnavailableException,並立即使用CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false)重試createCredential()。IllegalArgumentException:如果requestJson為空白、不是有效的 JSON,或沒有符合 WebAuthn 規格的有效 Base64URL 編碼user.id,就會發生這個例外狀況。
使用還原金鑰登入
在裝置設定程序中,使用還原憑證自動登入使用者。
從應用程式伺服器取得憑證擷取選項
在新裝置上呼叫 getCredential() 之前,請先向應用程式伺服器要求 WebAuthn PublicKeyCredentialRequestOptions JSON 驗證問題。由於新裝置上的應用程式還不知道是哪位使用者登入,因此這個端點必須未經驗證 (無使用者探索)。如需相關伺服器指南,請參閱「使用密碼金鑰登入」和伺服器端驗證指南。
檢索選項 JSON 的結構如下:
{
"challenge": "<your app challenge>",
"allowCredentials": [],
"rpId": "<your app server domain>"
}
請按照下列方式,在伺服器上設定檢索選項 JSON:
allowCredentials:請省略這個欄位或傳遞空白陣列 ([]),因為在還原前,系統無法得知使用者的帳戶。getCredential()傳回後,伺服器會從response.userHandle欄位 (與建立期間註冊的user.id相符) 識別使用者,並依id(credentialId) 查詢公開金鑰。userVerification:設為"discouraged"(或"preferred")。由於還原憑證會在背景執行,不會顯示生物辨識或螢幕鎖定 UI,因此 Android 會將"required"覆寫為"discouraged",且傳回伺服器的驗證器資料會將使用者驗證 (uv) 旗標設為false。設定 WebAuthn 伺服器驗證器,接受uv = false做為還原金鑰斷言。
取得還原金鑰
如要在新裝置上取得還原金鑰,請將伺服器的 requestJson 包裝在 GetCredentialRequest 內的 GetRestoreCredentialOption 中,然後在 CredentialManager 物件上呼叫 getCredential()。
實作兩層還原策略,無論背景應用程式資料還原是否執行,使用者都能登入:
- 第 1 級 (在
BackupAgent.onRestoreFinished()中背景還原):如果已啟用應用程式資料備份 (android:allowBackup="true"),請子類別化BackupAgent並覆寫onRestoreFinished()(請勿使用onRestore(),因為這只會用於舊版鍵/值備份)。由於onRestoreFinished()是同步函式,而getCredential()是 Kotlinsuspend函式,因此請使用runBlocking(Dispatchers.IO)橋接呼叫。在沒有還原金鑰時,安靜地擷取NoCredentialException。 - 第 2 層 (啟動器
Activity.onCreate()中的前景備用方案):在首次啟動應用程式時,如果沒有本機工作階段,請先從lifecycleScope呼叫getCredential(),再顯示登入畫面。這會處理含有android:allowBackup="false"的應用程式,並在onRestoreFinished()執行時裝置離線的情況下復原。
如果將新的 BackupAgent 新增至已使用 android:allowBackup="true" 自動備份的應用程式,請一併在 AndroidManifest.xml 的 <application> 元素上設定 android:fullBackupOnly="true",這樣 Android 就會繼續使用自動備份,而不是切換至舊版鍵/值備份。
suspend fun getRestoreKey(
fetchAuthenticationJson: () -> String,
) {
// Fetch the options required to get the restore key
val authenticationJson = fetchAuthenticationJson()
// Create the GetRestoreCredentialOption object
val options = GetRestoreCredentialOption(authenticationJson)
val getRequest = GetCredentialRequest(listOf(options))
try {
val response = credentialManager.getCredential(context, getRequest)
// Type-check and extract the restore credential
val credential = response.credential
if (credential is RestoreCredential) {
val responseJson = credential.authenticationResponseJson
}
} catch (e: NoCredentialException) {
// No restore key is present on this device
}
}
class MyBackupAgent : BackupAgent() {
override fun onRestoreFinished() {
super.onRestoreFinished()
val credentialManager = CredentialManager.create(this)
val restoreFunctions = RestoreCredentialsFunctions(this, credentialManager)
runBlocking(Dispatchers.IO) {
restoreFunctions.getRestoreKey(
fetchAuthenticationJson = {
/* Fetch challenge JSON from server */ ""
}
)
}
}
}
Credential Manager API 會傳回 GetCredentialResponse,其 credential 屬性是包含 authenticationResponseJson 的 RestoreCredential (而非 PublicKeyCredential)。
處理登入回應
將 restoreCredential.authenticationResponseJson 從應用程式傳送至依賴方伺服器,驗證密碼編譯斷言,並為使用者核發新的工作階段符記。如需伺服器端驗證的詳細資料,請參閱「使用密碼金鑰登入」。
設計還原金鑰的後端儲存空間和生命週期政策時,請遵循下列規範:
- 區分還原金鑰和使用者密碼金鑰:在資料庫中標記還原金鑰,這樣就不會顯示在面向使用者的密碼金鑰管理畫面,也不需要進行使用者驗證 (
uv = true) 檢查。 - 支援每位使用者擁有多部使用中的裝置:使用者可以擁有多部裝置 (例如手機和平板電腦)。允許每個使用者帳戶有多個有效的還原金鑰 (例如依裝置或安裝 ID 鍵入),而不是覆寫單一資料列。
- 設定適當的存留時間並清除孤立金鑰:解除安裝應用程式或清除應用程式資料會刪除本機還原金鑰,但不會通知伺服器。還原金鑰的保留時間應足以涵蓋裝置升級期間 (即使使用者在轉移後登出舊裝置),並根據閒置存留時間或每位使用者的金鑰數量上限,刪除過時的金鑰。
刪除還原金鑰
Credential Manager 是無狀態,且不會偵測使用者活動,因此不會在使用後自動刪除還原金鑰。如要刪除還原金鑰,請呼叫 clearCredentialState() 方法。基於安全考量,請在使用者登出時刪除金鑰,以免下次啟動應用程式時,系統在使用者不知情的情況下自動登入。
只有在下列情況下,系統才會移除還原金鑰:
- 系統層級動作:使用者解除安裝應用程式或清除資料。
- 應用程式層級呼叫:在應用程式程式碼中處理使用者登出時,呼叫
clearCredentialState(),以程式輔助方式刪除金鑰。
由於預設 ClearCredentialStateRequest() 只會清除標準工作階段狀態,並保留還原金鑰,因此請在登出時執行雙重清除:
- 呼叫
credentialManager.clearCredentialState(ClearCredentialStateRequest()),清除有效的憑證提供者工作階段狀態。 - 使用
ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL)呼叫credentialManager.clearCredentialState(),刪除儲存的RestoreCredential。 - 從本機儲存空間刪除本機
has_synced_restore_credential標記。
// Clear active credential provider session state
credentialManager.clearCredentialState(ClearCredentialStateRequest())
// When the user logs out, delete the restore key
val clearRequest = ClearCredentialStateRequest(TYPE_CLEAR_RESTORE_CREDENTIAL)
val response = credentialManager.clearCredentialState(clearRequest)