第75篇 | HarmonyOS 本地认证:人脸和指纹解锁保险箱的用户路径
第75篇 | HarmonyOS 本地认证:人脸和指纹解锁保险箱的用户路径
第 75 篇聚焦本地认证。保险箱不是把照片换个列表存放就结束,它必须在用户查看私密内容前确认身份。HarmonyOS 提供 UserAuthenticationKit,项目用它实现人脸、指纹和 PIN 的组合认证,并通过信任等级降级提高设备兼容性。
这一篇会从权限申请、随机挑战、认证实例、信任等级降级、解锁状态五个层次拆代码。重点不是记住每个枚举,而是理解一个“可用的保险箱解锁流程”应该如何处理权限、设备不支持、用户取消、服务忙和成功解锁。
本篇目标
- 理解
ACCESS_BIOMETRIC权限为什么要在解锁前申请。 - 掌握 UserAuthenticationKit 中 challenge、authType 和 trustLevel 的组合。
- 理解 ATL3/ATL2/ATL1 信任等级降级的设备兼容意义。
- 把认证结果映射成用户能理解的保险箱状态文案。
对应源码位置
superImage/entry/src/main/ets/pages/Index.etssuperImage/entry/src/main/module.json5
保险箱解锁是用户路径,不只是 API 调用
从运行页面看,保险箱锁定态提供“解锁保险箱”“人脸识别”“指纹识别”等入口。用户只关心自己是否能顺利进入私密照片,工程里却要处理权限、认证方式、信任等级和失败状态。
好的本地认证体验应该尽量少打扰,但失败时要说清楚原因。比如没有设置指纹、设备不支持某种认证方式、认证服务忙、用户取消,都不应该显示同一句“失败”。项目里的结果映射函数正是为这件事服务。

保险箱锁定态提供人脸和指纹认证入口
权限申请放在认证流程之前
requestBiometricPermission 先检查当前授权,如果已经授权直接返回;没有授权时调用 requestPermissionsFromUser,最后再复查一次授权状态。这个“双重确认”能覆盖用户在系统弹窗中授权后状态尚未及时同步的情况。
如果权限申请异常,函数会把错误码写入 vaultStatusText 并返回 false。调用方看到 false 后停止认证流程,避免在权限未准备好时继续创建认证实例。这样失败点更明确,用户也不会连续看到多个系统弹窗。

requestBiometricPermission 先检查授权,再请求用户授权
private async requestBiometricPermission(): Promise<boolean> {
const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
const hostContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
try {
const currentGrant = await this.checkPermissionGrant('ohos.permission.ACCESS_BIOMETRIC');
if (currentGrant === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
const result: PermissionRequestResult = await atManager.requestPermissionsFromUser(
hostContext,
this.biometricPermissionList
);
if (result.authResults.length > 0 &&
result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
const grantAfterResult = await this.checkPermissionGrant('ohos.permission.ACCESS_BIOMETRIC');
return grantAfterResult === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
const err = error as BusinessError;
this.vaultStatusText = `保险箱权限申请失败 ${err.code}`;
return false;
}
}
随机挑战和结果文案都要准备好
UserAuthenticationKit 认证需要 challenge。项目通过 cryptoFramework.createRandom 生成 16 字节随机挑战,并最多尝试三次。这样认证请求不是一个固定值,符合本地身份认证的基本安全要求。
getUserAuthResultMessage 则把枚举结果转换成中文状态。成功、失败、取消、超时、服务忙、锁定、未录入、不支持类型、不支持信任等级都分别处理。对于保险箱来说,失败文案越具体,用户越容易知道下一步该做什么。

本地认证前生成随机 challenge,并映射认证结果
private createUserAuthChallenge(): Uint8Array {
const challengeSize = 16;
let random: cryptoFramework.Random;
try {
random = cryptoFramework.createRandom();
} catch (error) {
const err = error as BusinessError;
throw new Error(`创建本地认证随机挑战失败:${err.message ?? err.code ?? 'unknown'}`);
}
for (let attempt = 0; attempt < 3; attempt++) {
try {
const randomData = random.generateRandomSync(challengeSize)?.data;
if (randomData && randomData.length > 0) {
return randomData;
}
} catch (error) {
console.error(`Failed to generate user auth challenge: ${JSON.stringify(error)}`);
}
}
throw new Error('');
}
private getUserAuthResultMessage(resultCode: number): string {
if (resultCode === userAuth.UserAuthResultCode.SUCCESS) {
return '认证成功';
}
if (resultCode === userAuth.UserAuthResultCode.FAIL) {
return '认证失败,请重试';
}
if (resultCode === userAuth.UserAuthResultCode.CANCELED) {
return '已取消认证';
}
if (resultCode === userAuth.UserAuthResultCode.TIMEOUT) {
return '认证超时';
}
if (resultCode === userAuth.UserAuthResultCode.BUSY) {
return '认证服务正忙,请稍后再试';
}
if (resultCode === userAuth.UserAuthResultCode.LOCKED) {
return '认证能力暂时锁定';
}
if (resultCode === userAuth.UserAuthResultCode.NOT_ENROLLED) {
return '还没有设置可用的本地身份认证';
}
if (resultCode === userAuth.UserAuthResultCode.TYPE_NOT_SUPPORT) {
return '当前设备不支持此认证方式';
}
if (resultCode === userAuth.UserAuthResultCode.TRUST_LEVEL_NOT_SUPPORT) {
return '当前安全等级不支持';
}
return `未知认证结果:${resultCode}`;
认证实例通过回调返回结果
authenticateWithLocalCredential 构造 AuthParam 和 WidgetParam,再通过 userAuth.getUserAuthInstance 创建认证实例。结果通过 onResult 回调返回,回调触发后立即 off,避免同一个回调重复响应。
这段代码还有一个细节:函数返回 Promise,把回调式认证封装成 async/await 可用的形式。页面解锁流程就能像普通异步函数一样顺序书写,状态设置、错误捕获和 finally 恢复也更清晰。

authenticateWithLocalCredential 把认证回调封装成 Promise
private authenticateWithLocalCredential(
authTypes: Array<userAuth.UserAuthType>,
title: string,
trustLevel: userAuth.AuthTrustLevel
): Promise<userAuth.UserAuthResult> {
return new Promise((resolve, reject) => {
try {
const authParam: userAuth.AuthParam = {
challenge: this.createUserAuthChallenge(),
authType: authTypes,
authTrustLevel: trustLevel
};
const widgetParam: userAuth.WidgetParam = {
title: title,
};
const userAuthInstance = userAuth.getUserAuthInstance(authParam, widgetParam);
const callback: userAuth.IAuthCallback = {
onResult: (result: userAuth.UserAuthResult): void => {
userAuthInstance.off('result', callback);
resolve(result);
}
};
userAuthInstance.on('result', callback);
userAuthInstance.start();
} catch (error) {
reject(error);
}
});
}
信任等级降级提高兼容性
不同设备支持的认证能力和信任等级不同。项目没有只尝试最高等级 ATL3,而是按 ATL3、ATL2、ATL1 顺序尝试。只有当结果不是“不支持信任等级”或“不支持类型”时才返回,否则继续尝试下一档。
authenticateVault 支持 PIN、FACE、FINGERPRINT;指纹入口则优先 FINGERPRINT,再回退 PIN。这个设计既尊重用户点击入口的预期,又保留必要的兜底,避免设备不支持某个生物能力时保险箱完全打不开。

authenticateWithTrustFallback 按信任等级逐级尝试
private async authenticateWithTrustFallback(
authTypes: Array<userAuth.UserAuthType>,
title: string
): Promise<userAuth.UserAuthResult> {
const trustLevels: Array<userAuth.AuthTrustLevel> = [
userAuth.AuthTrustLevel.ATL3,
userAuth.AuthTrustLevel.ATL2,
userAuth.AuthTrustLevel.ATL1
];
let lastErrorMessage = '';
for (const trustLevel of trustLevels) {
try {
const result = await this.authenticateWithLocalCredential(authTypes, title, trustLevel);
if (result.result !== userAuth.UserAuthResultCode.TRUST_LEVEL_NOT_SUPPORT &&
result.result !== userAuth.UserAuthResultCode.TYPE_NOT_SUPPORT) {
return result;
}
} catch (error) {
const err = error as BusinessError;
lastErrorMessage = `${err.message ?? err.code ?? 'unknown'}`;
}
}
if (lastErrorMessage.length > 0) {
throw new Error(lastErrorMessage);
}
return {
result: userAuth.UserAuthResultCode.TRUST_LEVEL_NOT_SUPPORT,
token: new Uint8Array()
};
}
private authenticateVault(): Promise<userAuth.UserAuthResult> {
return this.authenticateWithTrustFallback([
userAuth.UserAuthType.PIN,
userAuth.UserAuthType.FACE,
userAuth.UserAuthType.FINGERPRINT
], '验证身份');
}
private authenticateVaultWithFingerprint(): Promise<userAuth.UserAuthResult> {
return this.authenticateWithTrustFallback([
userAuth.UserAuthType.FINGERPRINT,
userAuth.UserAuthType.PIN
], '指纹解锁保险箱');
工程检查清单
module.json5中声明ohos.permission.ACCESS_BIOMETRIC。- 认证前先申请权限,权限失败时停止后续流程。
- 认证回调触发后要取消监听。
- 不同认证类型和信任等级都要有可读失败文案。
今日练习
- 在没有录入指纹的设备上点击指纹入口,观察返回文案。
- 临时只保留 ATL3,比较不支持设备上的失败表现。
- 把
vaultAuthBusy打印出来,确认连续点击不会并发拉起认证。
训练营里的每一篇都建议按同一个节奏复盘:先看页面行为,再回到源码定位状态和服务层,最后自己改一个很小的参数验证结果。这样写文章时不会停留在 API 名词,读者也能沿着真实工程把功能跑通。
更多推荐

所有评论(0)