生物识别
源:Android BiometricPrompt | iOS LocalAuthentication
生物识别(指纹、面容 ID)是现代应用的必备安全功能。本文将展示如何跨平台统一生物识别 API。
平台差异对比
| 平台 | 原生 API | 支持类型 | 权限要求 | 降级方案 |
|---|---|---|---|---|
| Android | BiometricPrompt | 指纹、面容、虹膜 | USE_BIOMETRIC | PIN/密码 |
| iOS | LAContext | Touch ID、Face ID | Info.plist 描述 | 设备密码 |
| Desktop | 无原生支持 | - | - | 密码认证 |
expect/actual 实现方案
API 核心签名说明
enum class BiometricResultexpect class BiometricAuthenticatorsuspend fun BiometricAuthenticator.authenticate(title: String, subtitle: String): BiometricResultsuspend fun BiometricAuthenticator.canAuthenticate(): Boolean
标准代码块
kotlin
enum class BiometricResult {
SUCCESS, // 认证成功
FAILED, // 认证失败(指纹不匹配)
ERROR, // 系统错误
CANCELED, // 用户取消
NOT_AVAILABLE // 设备不支持或未设置
}
expect class BiometricAuthenticator {
suspend fun canAuthenticate(): Boolean
suspend fun authenticate(
title: String,
subtitle: String = "",
description: String = ""
): BiometricResult
}
// 业务层使用
class SecureFeature(private val biometric: BiometricAuthenticator) {
suspend fun unlockSecureData(): Boolean {
if (!biometric.canAuthenticate()) {
// 降级到密码认证
return false
}
val result = biometric.authenticate(
title = "验证身份",
subtitle = "使用指纹或面容解锁",
description = "验证后可查看敏感信息"
)
return result == BiometricResult.SUCCESS
}
}kotlin
import android.content.Context
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
actual class BiometricAuthenticator(private val activity: FragmentActivity) {
private val biometricManager = BiometricManager.from(activity)
actual suspend fun canAuthenticate(): Boolean {
return when (biometricManager.canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG
)) {
BiometricManager.BIOMETRIC_SUCCESS -> true
else -> false
}
}
actual suspend fun authenticate(
title: String,
subtitle: String,
description: String
): BiometricResult = suspendCancellableCoroutine { continuation ->
val executor = ContextCompat.getMainExecutor(activity)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
continuation.resume(BiometricResult.SUCCESS)
}
override fun onAuthenticationFailed() {
// 单次失败,但可以继续尝试
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
val result = when (errorCode) {
BiometricPrompt.ERROR_USER_CANCELED,
BiometricPrompt.ERROR_CANCELED -> BiometricResult.CANCELED
BiometricPrompt.ERROR_NO_BIOMETRICS,
BiometricPrompt.ERROR_HW_NOT_PRESENT -> BiometricResult.NOT_AVAILABLE
else -> BiometricResult.ERROR
}
continuation.resume(result)
}
}
val prompt = BiometricPrompt(activity, executor, callback)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setDescription(description)
.setNegativeButtonText("取消")
.build()
prompt.authenticate(promptInfo)
}
}kotlin
import platform.LocalAuthentication.*
import platform.Foundation.NSError
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
actual class BiometricAuthenticator {
private val context = LAContext()
actual suspend fun canAuthenticate(): Boolean {
val error: NSError? = null
return context.canEvaluatePolicy(
LAPolicyDeviceOwnerAuthenticationWithBiometrics,
error = error
)
}
actual suspend fun authenticate(
title: String,
subtitle: String,
description: String
): BiometricResult = suspendCancellableCoroutine { continuation ->
val reason = buildString {
append(title)
if (subtitle.isNotEmpty()) {
append("\n")
append(subtitle)
}
if (description.isNotEmpty()) {
append("\n")
append(description)
}
}
context.evaluatePolicy(
LAPolicyDeviceOwnerAuthenticationWithBiometrics,
localizedReason = reason
) { success, error ->
val result = when {
success -> BiometricResult.SUCCESS
error != null -> {
when (error.code) {
LAErrorUserCancel -> BiometricResult.CANCELED
LAErrorBiometryNotAvailable,
LAErrorBiometryNotEnrolled -> BiometricResult.NOT_AVAILABLE
LAErrorAuthenticationFailed -> BiometricResult.FAILED
else -> BiometricResult.ERROR
}
}
else -> BiometricResult.ERROR
}
continuation.resume(result)
}
}
}kotlin
actual class BiometricAuthenticator {
actual suspend fun canAuthenticate(): Boolean {
// Desktop 不支持生物识别
return false
}
actual suspend fun authenticate(
title: String,
subtitle: String,
description: String
): BiometricResult {
return BiometricResult.NOT_AVAILABLE
}
}代码封装示例
以下是带密钥绑定和降级的完整封装:
kotlin
// commonMain
sealed class AuthResult {
data class Success(val cryptoObject: Any? = null) : AuthResult()
data class Failure(val reason: BiometricResult) : AuthResult()
}
class SecureAuthenticator(
private val biometric: BiometricAuthenticator,
private val passwordAuth: suspend (String) -> Boolean
) {
suspend fun authenticateWithFallback(
title: String,
allowPassword: Boolean = true
): AuthResult {
// 优先尝试生物识别
if (biometric.canAuthenticate()) {
val result = biometric.authenticate(title)
if (result == BiometricResult.SUCCESS) {
return AuthResult.Success()
}
// 生物识别失败,是否允许降级
if (!allowPassword || result == BiometricResult.CANCELED) {
return AuthResult.Failure(result)
}
}
// 降级到密码认证
// 实际应用中应显示密码输入对话框
return AuthResult.Failure(BiometricResult.NOT_AVAILABLE)
}
}依赖补充
Android BiometricPrompt
kotlin
kotlin {
sourceSets {
androidMain.dependencies {
implementation("androidx.biometric:biometric:1.2.0-alpha05")
}
}
}groovy
kotlin {
sourceSets {
androidMain {
dependencies {
implementation "androidx.biometric:biometric:1.2.0-alpha05"
}
}
}
}toml
[versions]
androidx-biometric = "1.2.0-alpha05"
[libraries]
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "androidx-biometric" }最新版本查看链接:https://developer.android.com/jetpack/androidx/releases/biometric
AndroidManifest.xml 配置
xml
<uses-permission android:name="android.permission.USE_BIOMETRIC" />iOS Info.plist 配置
xml
<key>NSFaceIDUsageDescription</key>
<string>使用 Face ID 验证您的身份</string>实战坑点
Android FragmentActivity 依赖
BiometricPrompt 需要 FragmentActivity
BiometricPrompt 构造函数要求 FragmentActivity,不能使用普通 Context。
解决方案:
kotlin
// 确保传入 FragmentActivity
class BiometricAuthenticator(private val activity: FragmentActivity) {
// ...
}
// 在 Activity 中初始化
class MainActivity : FragmentActivity() {
private val biometric = BiometricAuthenticator(this)
}iOS 主线程回调
回调线程问题
iOS LocalAuthentication 的回调可能在后台线程执行。
解决方案:
kotlin
context.evaluatePolicy(...) { success, error ->
// 切换到主线程
dispatch_async(dispatch_get_main_queue()) {
// 更新 UI
}
}Android 降级按钮
设备凭据降级
可以配置 BiometricPrompt 在失败后显示 PIN/密码输入。
配置方法:
kotlin
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("验证身份")
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
// 使用 DEVICE_CREDENTIAL 时不能设置 NegativeButton
.build()iOS Face ID 权限描述缺失
应用崩溃
未在 Info.plist 添加 NSFaceIDUsageDescription 会导致崩溃。
解决方案:
始终添加权限描述,即使设备不支持 Face ID:
xml
<key>NSFaceIDUsageDescription</key>
<string>使用 Face ID 快速登录</string>设备未设置生物识别
::: caution 用户体验 设备支持但未设置生物识别时,应引导用户设置。 :::
检测与引导:
kotlin
// Android
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// 引导用户前往设置添加指纹
val intent = Intent(Settings.ACTION_BIOMETRIC_ENROLL)
startActivity(intent)
}
}