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

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

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

如要定義應用程式支援的驗證類型,請使用 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 的生物特徵辨識驗證,請參閱下列資源。

網誌文章