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

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

一般來說,您應使用 Credential Manager 在裝置上首次登入。後續的重新授權作業可以透過生物特徵辨識提示或憑證管理工具完成。使用生物特徵提示的好處是,它提供更多自訂選項,而憑證管理工具則可在兩個流程中提供單一實作。

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

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

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

如要開始使用驗證器,使用者必須建立 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。如要在 Android 10 以下版本中檢查 PIN 碼、解鎖圖案或密碼是否存在,請使用 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 例項,將加密編譯作業併入生物特徵辨識驗證工作流程。這個架構支援以下加密編譯物件:SignatureCipherMac

使用者透過生物特徵辨識提示順利完成驗證後,應用程式就可以執行加密編譯作業。舉例來說,假如您使用 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();

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

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

但是如果應用程式會針對低風險動作顯示生物特徵辨識驗證對話方塊,您可以向系統提供提示,讓使用者不必確認驗證。這類提示可讓使用者能夠在透過被動式模態 (例如臉孔或瞳孔辨識) 進行重新驗證後,更快看到應用程式中的內容。如要提供這類提示,請將 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 系統的生物特徵辨識驗證功能,請參閱下列資源。

網誌文章