Google 致力于为黑人社区推动种族平等。查看具体举措

显示生物识别身份验证对话框

如需保护您的应用中的敏感信息或付费内容,一种方法是请求生物识别身份验证,例如使用人脸识别或指纹识别。本指南介绍了如何在您的应用中支持生物识别登录流程。

声明您的应用支持的身份验证类型

如需定义您的应用支持的身份验证类型,请使用 BiometricManager.Authenticators 接口。系统允许您声明以下类型的身份验证:

BIOMETRIC_STRONG
使用满足兼容性定义页面上定义的强度级别要求的硬件元素进行身份验证。
BIOMETRIC_WEAK
使用满足兼容性定义页面上定义的强度级别要求的硬件元素进行身份验证。
DEVICE_CREDENTIAL
使用屏幕锁定凭据(即用户的 PIN 码、解锁图案或密码)进行身份验证。

为了注册身份验证器,用户需要创建 PIN 码、解锁图案或密码。如果用户尚无 PIN 码、解锁图案或密码,生物识别注册流程会提示他们创建一个。

如需定义您的应用接受的生物识别身份验证类型,请向 setAllowedAuthenticators() 方法传递一个身份验证类型或按位类型组合。以下代码段展示了如何使用“强”硬件元素或屏幕锁定凭据来支持身份验证。

Kotlin

// Allows user to authenticate using either a "strong" hardware element 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")
        // Can't call setNegativeButtonText() and
        // setAllowedAuthenticators(... or DEVICE_CREDENTIAL) at the same time.
        // .setNegativeButtonText("Use account password")
        .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        .build()

Java

// Allows user to authenticate using either a "strong" hardware element 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")
        // Can't call setNegativeButtonText() and
        // setAllowedAuthenticators(...|DEVICE_CREDENTIAL) at the same time.
        // .setNegativeButtonText("Use account password")
        .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
        .build();

注意:Android 10(API 级别 29)及更低版本不支持某些身份验证器类型组合:

  • DEVICE_CREDENTIAL
  • BIOMETRIC_STRONG | DEVICE_CREDENTIAL

如需在 Android 10 及更低版本中检查是否存在 PIN 码、解锁图案或密码,请使用 KeyguardManager.isDeviceSecure()

检查生物识别身份验证是否可用

确定您的应用支持的身份验证元素后,请检查这些元素是否可用。为此,请将先前声明的同一按位类型组合传递到 canAuthenticate() 方法中。如有必要,请调用 ACTION_BIOMETRIC_ENROLL intent 操作。在 intent extra 中,提供您的应用可接受的一组身份验证器。该 intent 会提示用户为您的应用接受的身份验证器注册凭据。

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() 检查用户是使用设备凭据还是生物识别凭据进行的身份验证。

显示登录提示

如需显示请求用户使用生物识别凭据进行身份验证的系统提示,请使用 Biometric 库。这个由系统提供的对话框在使用它的各个应用之间均保持一致,从而打造更值得信赖的用户体验。图 1 中显示了一个示例对话框。

显示对话框的屏幕截图
图 1. 请求生物识别身份验证的系统对话框

如需使用 Biometric 库向应用添加生物识别身份验证,请按照以下步骤操作:

  1. 在应用的 app/build.gradle 文件中,添加 Biometric 库的依赖项:

    dependencies {
        implementation 'androidx.biometric:biometric:1.0.1'
    }
    
  2. 在托管生物识别登录对话框的 Activity 或 Fragment 中,使用以下代码段中所示的逻辑显示对话框:

    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.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.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.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.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

// Allows user to 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

// Allows user to 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 设备上的生物识别身份验证,请参阅以下资源。

博文