顯示生物特徵辨識驗證對話方塊

如要保護應用程式中的機密資訊或付費內容,其中一種方法是要求進行生物特徵辨識驗證,例如使用臉孔/指紋辨識功能。本指南說明如何在應用程式中支援生物特徵辨識登入流程。

一般來說,您應使用 Credential Manager 在裝置上首次登入。如有需要,您可以使用生物特徵辨識提示、 或 Credential Manager使用生物特徵辨識提示的好處 更多自訂選項,Credential Manager 則單一 並在兩個流程中實作

宣告應用程式支援的驗證類型

如要定義應用程式支援的驗證類型,請使用 BiometricManager.Authenticators 介面。系統可讓您宣告 驗證:

BIOMETRIC_STRONG
使用第 3 級生物特徵辨識 (如 Android 相容性定義頁面所定義) 進行驗證。
BIOMETRIC_WEAK
使用第 2 級生物特徵辨識 (如 Android 相容性定義頁面所定義) 進行驗證。
DEVICE_CREDENTIAL
使用螢幕鎖定憑證進行驗證,例如使用者的 PIN 碼、解鎖圖案或 密碼。

如要開始使用驗證器,使用者必須先建立 PIN 碼 解鎖圖案或密碼如果使用者還沒有 註冊流程會提示使用者建立帳戶

如要定義應用程式接受的生物特徵辨識驗證類型,請傳遞 或混合形式的類型 setAllowedAuthenticators() 方法。下列程式碼片段說明如何使用第 3 級生物特徵辨識或螢幕鎖定憑證來支援驗證作業。

Kotlin

// Lets the user authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        .build()

Java

// Lets user authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = new BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
        .build();

Android 10 (API 級別 29) 以下版本不支援下列驗證器類型組合:DEVICE_CREDENTIALBIOMETRIC_STRONG | DEVICE_CREDENTIAL。如要檢查 PIN 碼是否出現, 解鎖 Android 10 以下版本的解鎖圖案或密碼,並使用 KeyguardManager.isDeviceSecure() 方法。

檢查是否提供生物特徵辨識驗證

決定應用程式支援哪些驗證元素後,請確認應用程式是否支援 可以使用這些元素檢查方式為將使用 setAllowedAuthenticators() 方法宣告的相同位元類型組合傳遞至 canAuthenticate() 方法。如有需要,請叫用 ACTION_BIOMETRIC_ENROLL 意圖 動作。在意圖的附加資料中,請提供應用程式接受的驗證器組合。這項意圖會提示使用者針對應用程式接受的驗證器註冊憑證。

Kotlin

val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
    BiometricManager.BIOMETRIC_SUCCESS ->
        Log.d("MY_APP_TAG", "App can authenticate using biometrics.")
    BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
        Log.e("MY_APP_TAG", "No biometric features available on this device.")
    BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
        Log.e("MY_APP_TAG", "Biometric features are currently unavailable.")
    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
        // Prompts the user to create credentials that your app accepts.
        val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
            putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        }
        startActivityForResult(enrollIntent, REQUEST_CODE)
    }
}

Java

BiometricManager biometricManager = BiometricManager.from(this);
switch (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)) {
    case BiometricManager.BIOMETRIC_SUCCESS:
        Log.d("MY_APP_TAG", "App can authenticate using biometrics.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
        Log.e("MY_APP_TAG", "No biometric features available on this device.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
        Log.e("MY_APP_TAG", "Biometric features are currently unavailable.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
        // Prompts the user to create credentials that your app accepts.
        final Intent enrollIntent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL);
        enrollIntent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                BIOMETRIC_STRONG | DEVICE_CREDENTIAL);
        startActivityForResult(enrollIntent, REQUEST_CODE);
        break;
}

判斷使用者的驗證方式

使用者通過驗證後,您可以使用 取得裝置憑證或生物特徵辨識憑證 getAuthenticationType()

顯示登入提示

如要顯示系統提示,要求使用者透過生物特徵辨識憑證進行驗證,請使用生物特徵辨識程式庫。這個 系統提供的對話方塊在使用此功能的所有應用程式中都會保持一致, 提供更可靠的使用者體驗圖 1 顯示的是其中一種對話方塊範例。

顯示對話方塊的螢幕截圖
圖 1. 要求生物特徵辨識驗證的系統對話方塊。

如要使用生物特徵辨識程式庫為應用程式新增生物特徵辨識驗證,請完成下列步驟:

  1. 在應用程式模組的 build.gradle 檔案中,新增 androidx.biometric 程式庫的依附元件

  2. 在代管生物特徵辨識登入作業的活動或片段中,使用以下程式碼片段所示的邏輯顯示對話方塊:

    Kotlin

    private lateinit var executor: Executor
    private lateinit var biometricPrompt: BiometricPrompt
    private lateinit var promptInfo: BiometricPrompt.PromptInfo
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        executor = ContextCompat.getMainExecutor(this)
        biometricPrompt = BiometricPrompt(this, executor,
                object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int,
                    errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                Toast.makeText(applicationContext,
                    "Authentication error: $errString", Toast.LENGTH_SHORT)
                    .show()
            }
    
            override fun onAuthenticationSucceeded(
                    result: BiometricPrompt.AuthenticationResult) {
                super.onAuthenticationSucceeded(result)
                Toast.makeText(applicationContext,
                    "Authentication succeeded!", Toast.LENGTH_SHORT)
                    .show()
            }
    
            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                Toast.makeText(applicationContext, "Authentication failed",
                    Toast.LENGTH_SHORT)
                    .show()
            }
        })
    
        promptInfo = BiometricPrompt.PromptInfo.Builder()
                .setTitle("Biometric login for my app")
                .setSubtitle("Log in using your biometric credential")
                .setNegativeButtonText("Use account password")
                .build()
    
        // Prompt appears when user clicks "Log in".
        // Consider integrating with the keystore to unlock cryptographic operations,
        // if needed by your app.
        val biometricLoginButton =
                findViewById<Button>(R.id.biometric_login)
        biometricLoginButton.setOnClickListener {
            biometricPrompt.authenticate(promptInfo)
        }
    }
    

    Java

    private Executor executor;
    private BiometricPrompt biometricPrompt;
    private BiometricPrompt.PromptInfo promptInfo;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        executor = ContextCompat.getMainExecutor(this);
        biometricPrompt = new BiometricPrompt(MainActivity.this,
                executor, new BiometricPrompt.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode,
                    @NonNull CharSequence errString) {
                super.onAuthenticationError(errorCode, errString);
                Toast.makeText(getApplicationContext(),
                    "Authentication error: " + errString, Toast.LENGTH_SHORT)
                    .show();
            }
    
            @Override
            public void onAuthenticationSucceeded(
                    @NonNull BiometricPrompt.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                Toast.makeText(getApplicationContext(),
                    "Authentication succeeded!", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            public void onAuthenticationFailed() {
                super.onAuthenticationFailed();
                Toast.makeText(getApplicationContext(), "Authentication failed",
                    Toast.LENGTH_SHORT)
                    .show();
            }
        });
    
        promptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle("Biometric login for my app")
                .setSubtitle("Log in using your biometric credential")
                .setNegativeButtonText("Use account password")
                .build();
    
        // Prompt appears when user clicks "Log in".
        // Consider integrating with the keystore to unlock cryptographic operations,
        // if needed by your app.
        Button biometricLoginButton = findViewById(R.id.biometric_login);
        biometricLoginButton.setOnClickListener(view -> {
                biometricPrompt.authenticate(promptInfo);
        });
    }
    

使用需要驗證的加密編譯解決方案

如要進一步保護應用程式中的機密資訊,您可以使用 CryptoObject 例項,將加密編譯作業併入生物特徵辨識驗證工作流程。這個架構支援以下加密編譯物件: Signature, CipherMac

使用者透過生物特徵辨識提示順利完成驗證後,應用程式 執行加密編譯作業舉例來說,假如您使用 Cipher 物件進行驗證,那麼應用程式就可以使用 SecretKey 物件執行加密和解密作業。

以下各節逐一說明使用 Cipher 物件和 用於加密資料的 SecretKey 物件。每個範例都會使用下列方法:

Kotlin

private fun generateSecretKey(keyGenParameterSpec: KeyGenParameterSpec) {
    val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    keyGenerator.init(keyGenParameterSpec)
    keyGenerator.generateKey()
}

private fun getSecretKey(): SecretKey {
    val keyStore = KeyStore.getInstance("AndroidKeyStore")

    // Before the keystore can be accessed, it must be loaded.
    keyStore.load(null)
    return keyStore.getKey(KEY_NAME, null) as SecretKey
}

private fun getCipher(): Cipher {
    return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/"
            + KeyProperties.ENCRYPTION_PADDING_PKCS7)
}

Java

private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) {
    KeyGenerator keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    keyGenerator.init(keyGenParameterSpec);
    keyGenerator.generateKey();
}

private SecretKey getSecretKey() {
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");

    // Before the keystore can be accessed, it must be loaded.
    keyStore.load(null);
    return ((SecretKey)keyStore.getKey(KEY_NAME, null));
}

private Cipher getCipher() {
    return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/"
            + KeyProperties.ENCRYPTION_PADDING_PKCS7);
}

僅使用生物特徵辨識憑證進行驗證

如果您的應用程式使用需要生物特徵辨識憑證才能解鎖的密鑰, 使用者每次都必須驗證自己的生物特徵辨識憑證,才能啟動應用程式 存取金鑰。

如要在使用者透過生物特徵辨識憑證完成驗證後才加密機密資訊,請完成下列步驟:

  1. 產生採用下列項目的金鑰 KeyGenParameterSpec 設定:

    Kotlin

    generateSecretKey(KeyGenParameterSpec.Builder(
            KEY_NAME,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            // Invalidate the keys if the user has registered a new biometric
            // credential, such as a new fingerprint. Can call this method only
            // on Android 7.0 (API level 24) or higher. The variable
            // "invalidatedByBiometricEnrollment" is true by default.
            .setInvalidatedByBiometricEnrollment(true)
            .build())
    

    Java

    generateSecretKey(new KeyGenParameterSpec.Builder(
            KEY_NAME,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            // Invalidate the keys if the user has registered a new biometric
            // credential, such as a new fingerprint. Can call this method only
            // on Android 7.0 (API level 24) or higher. The variable
            // "invalidatedByBiometricEnrollment" is true by default.
            .setInvalidatedByBiometricEnrollment(true)
            .build());
    
  2. 啟動包含加密作業的生物特徵辨識驗證工作流程:

    Kotlin

    biometricLoginButton.setOnClickListener {
        // Exceptions are unhandled within this snippet.
        val cipher = getCipher()
        val secretKey = getSecretKey()
        cipher.init(Cipher.ENCRYPT_MODE, secretKey)
        biometricPrompt.authenticate(promptInfo,
                BiometricPrompt.CryptoObject(cipher))
    }
    

    Java

    biometricLoginButton.setOnClickListener(view -> {
        // Exceptions are unhandled within this snippet.
        Cipher cipher = getCipher();
        SecretKey secretKey = getSecretKey();
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        biometricPrompt.authenticate(promptInfo,
                new BiometricPrompt.CryptoObject(cipher));
    });
    
  3. 在生物特徵辨識驗證的回呼中,使用密鑰來加密 私密資訊:

    Kotlin

    override fun onAuthenticationSucceeded(
            result: BiometricPrompt.AuthenticationResult) {
        val encryptedInfo: ByteArray = result.cryptoObject.cipher?.doFinal(
            // plaintext-string text is whatever data the developer would like
            // to encrypt. It happens to be plain-text in this example, but it
            // can be anything
                plaintext-string.toByteArray(Charset.defaultCharset())
        )
        Log.d("MY_APP_TAG", "Encrypted information: " +
                Arrays.toString(encryptedInfo))
    }
    

    Java

    @Override
    public void onAuthenticationSucceeded(
            @NonNull BiometricPrompt.AuthenticationResult result) {
        // NullPointerException is unhandled; use Objects.requireNonNull().
        byte[] encryptedInfo = result.getCryptoObject().getCipher().doFinal(
            // plaintext-string text is whatever data the developer would like
            // to encrypt. It happens to be plain-text in this example, but it
            // can be anything
                plaintext-string.getBytes(Charset.defaultCharset()));
        Log.d("MY_APP_TAG", "Encrypted information: " +
                Arrays.toString(encryptedInfo));
    }

使用生物特徵辨識或螢幕鎖定憑證進行驗證

您可以使用允許透過生物特徵辨識進行驗證的密鑰 憑證或螢幕鎖定憑證 (PIN 碼、解鎖圖案或密碼),設定這組金鑰時,請指定有效期間。在此期間,應用程式可以執行多次加密編譯作業,使用者不必重新進行驗證。

在使用者透過生物特徵辨識或生物特徵辨識完成驗證後,將機密資訊加密 ,請完成下列步驟:

  1. 產生採用下列項目的金鑰 KeyGenParameterSpec 設定:

    Kotlin

    generateSecretKey(KeyGenParameterSpec.Builder(
        KEY_NAME,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS,
                ALLOWED_AUTHENTICATORS)
        .build())
    

    Java

    generateSecretKey(new KeyGenParameterSpec.Builder(
        KEY_NAME,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS,
                ALLOWED_AUTHENTICATORS)
        .build());
    
  2. 在使用者完成驗證後的 VALIDITY_DURATION_SECONDS 期間內,加密機密資訊:

    Kotlin

    private fun encryptSecretInformation() {
        // Exceptions are unhandled for getCipher() and getSecretKey().
        val cipher = getCipher()
        val secretKey = getSecretKey()
        try {
            cipher.init(Cipher.ENCRYPT_MODE, secretKey)
            val encryptedInfo: ByteArray = cipher.doFinal(
                // plaintext-string text is whatever data the developer would
                // like to encrypt. It happens to be plain-text in this example,
                // but it can be anything
                    plaintext-string.toByteArray(Charset.defaultCharset()))
            Log.d("MY_APP_TAG", "Encrypted information: " +
                    Arrays.toString(encryptedInfo))
        } catch (e: InvalidKeyException) {
            Log.e("MY_APP_TAG", "Key is invalid.")
        } catch (e: UserNotAuthenticatedException) {
            Log.d("MY_APP_TAG", "The key's validity timed out.")
            biometricPrompt.authenticate(promptInfo)
        }
    

    Java

    private void encryptSecretInformation() {
        // Exceptions are unhandled for getCipher() and getSecretKey().
        Cipher cipher = getCipher();
        SecretKey secretKey = getSecretKey();
        try {
            // NullPointerException is unhandled; use Objects.requireNonNull().
            ciper.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encryptedInfo = cipher.doFinal(
                // plaintext-string text is whatever data the developer would
                // like to encrypt. It happens to be plain-text in this example,
                // but it can be anything
                    plaintext-string.getBytes(Charset.defaultCharset()));
        } catch (InvalidKeyException e) {
            Log.e("MY_APP_TAG", "Key is invalid.");
        } catch (UserNotAuthenticatedException e) {
            Log.d("MY_APP_TAG", "The key's validity timed out.");
            biometricPrompt.authenticate(promptInfo);
        }
    }
    

使用單次使用驗證金鑰進行驗證

您可以在自己的執行個體中,支援單次使用驗證金鑰 BiometricPrompt。每當應用程式需要存取受這類金鑰保護的資料時,金鑰就會要求使用者提供生物特徵辨識憑證或裝置憑證。單次使用驗證金鑰適用於高額交易,例如 支付大額款項或更新某人的健康記錄。

如要將 BiometricPrompt 物件與單次使用驗證金鑰建立關聯,請新增程式碼 類似這樣:

Kotlin

val authPerOpKeyGenParameterSpec =
        KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose)
    // Accept either a biometric credential or a device credential.
    // To accept only one type of credential, include only that type as the
    // second argument.
    .setUserAuthenticationParameters(0 /* duration */,
            KeyProperties.AUTH_BIOMETRIC_STRONG or
            KeyProperties.AUTH_DEVICE_CREDENTIAL)
    .build()

Java

KeyGenParameterSpec authPerOpKeyGenParameterSpec =
        new KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose)
    // Accept either a biometric credential or a device credential.
    // To accept only one type of credential, include only that type as the
    // second argument.
    .setUserAuthenticationParameters(0 /* duration */,
            KeyProperties.AUTH_BIOMETRIC_STRONG |
            KeyProperties.AUTH_DEVICE_CREDENTIAL)
    .build();

在使用者未明確採取動作的情況下進行驗證

根據預設,系統會要求使用者執行特定動作,例如 使用者按一下按鈕 (接受其生物特徵辨識憑證)。如果應用程式會顯示對話方塊來確認敏感或高風險動作 (例如購買商品),就建議您採用這項設定。

但是如果應用程式會針對低風險動作顯示生物特徵辨識驗證對話方塊,您可以向系統提供提示,讓使用者不必確認驗證。這項提示可讓使用者查看您應用程式中的內容 使用被動式形態 (例如臉部或 以及以 iris 為基礎的辨識機制如要提供這類提示,請將 false 傳入 setConfirmationRequired() 方法。

圖 2 顯示同一個對話方塊的兩個版本。其中一個版本需要 使用者動作,另一個版本則沒有。

對話方塊的螢幕截圖 對話方塊的螢幕截圖
圖 2. 在使用者未確認 (上圖) 及 使用者確認 (下方)。

下列程式碼片段說明如何讓顯示的對話方塊,在「沒有」要求使用者明確採取動作的情況下完成驗證程序:

Kotlin

// Lets the user authenticate without performing an action, such as pressing a
// button, after their biometric credential is accepted.
promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setNegativeButtonText("Use account password")
        .setConfirmationRequired(false)
        .build()

Java

// Lets the user authenticate without performing an action, such as pressing a
// button, after their biometric credential is accepted.
promptInfo = new BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setNegativeButtonText("Use account password")
        .setConfirmationRequired(false)
        .build();

允許採用非生物特徵辨識憑證做為備用驗證方式

如果想讓應用程式使用生物特徵辨識憑證或裝置憑證來進行驗證,您可以在傳入 setAllowedAuthenticators() 的值組中加入 DEVICE_CREDENTIAL,藉此宣告應用程式支援裝置憑證

如果應用程式目前使用 createConfirmDeviceCredentialIntent()setDeviceCredentialAllowed() 如要提供這項功能,請改用 setAllowedAuthenticators()

其他資源

如要進一步瞭解 Android 系統的生物特徵辨識驗證功能,請參閱下列資源。

網誌文章