保护应用中的敏感信息或高级内容的一种方法是请求生物识别身份验证,例如使用面部识别或指纹识别。本指南说明如何在您的应用中支持生物识别登录流程。
一般来说,您应该使用 凭据管理器 在设备上进行初始登录。后续重新授权可以使用生物识别提示或凭据管理器完成。使用生物识别提示的优势在于它提供了更多自定义选项,而凭据管理器则在两种流程中都提供了单一实现。
声明您的应用支持的身份验证类型
要定义您的应用支持的身份验证类型,请使用 BiometricManager.Authenticators
接口。系统允许您声明以下类型的身份验证
BIOMETRIC_STRONG
- 使用 Android 兼容性定义 页面上定义的**3 类**生物识别技术进行身份验证。
BIOMETRIC_WEAK
- 使用 Android 兼容性定义 页面上定义的**2 类**生物识别技术进行身份验证。
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_CREDENTIAL
和 BIOMETRIC_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()
来检查用户是使用设备凭据还是生物识别凭据进行身份验证的。
显示登录提示
要显示一个系统提示,要求用户使用生物识别凭据进行身份验证,请使用 Biometric 库。此系统提供的对话框在使用它的应用中保持一致,从而创建更值得信赖的用户体验。图 1 显示了一个示例对话框。
要使用 Biometric 库向应用添加生物识别身份验证,请完成以下步骤
在应用模块的
build.gradle
文件中,添加对androidx.biometric
库的依赖项。在承载生物识别登录对话框的活动或片段中,使用以下代码片段中显示的逻辑显示对话框
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
、Cipher
和 Mac
。
用户使用生物识别提示成功进行身份验证后,应用可以执行加密操作。例如,如果使用 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); }
仅使用生物识别凭据进行身份验证
如果应用使用需要生物识别凭据才能解锁的密钥,则用户必须每次在应用访问密钥之前对其生物识别凭据进行身份验证。
要仅在用户使用生物识别凭据进行身份验证后加密敏感信息,请完成以下步骤
生成一个使用以下
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());
启动包含密码的生物识别身份验证工作流
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)); });
在生物识别身份验证回调中,使用密钥加密敏感信息
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 码、图案或密码)进行身份验证的密钥。配置此密钥时,请指定有效期。在此期间,您的应用可以执行多次加密操作,而无需用户重新进行身份验证。
要加密用户使用生物识别凭据或锁屏凭据进行身份验证后的敏感信息,请完成以下步骤
生成一个使用以下
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());
在用户身份验证后
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 显示了同一对话框的两个版本。一个版本需要显式用户操作,另一个版本不需要。
以下代码片段显示了如何呈现一个不需要显式用户操作即可完成身份验证过程的对话框
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 上生物识别身份验证的更多信息,请查阅以下资源。