HarmonyOS Next中用户认证场景下的关键资产保护实践 原创
本文旨在深入探讨华为鸿蒙HarmonyOS Next系统(截止目前 API12)在开发多语言电商平台方面的技术细节,基于实际开发实践进行总结。主要作为技术分享与交流载体,难免错漏,欢迎各位同仁提出宝贵意见和问题,以便共同进步。本文为原创内容,任何形式的转载必须注明出处及原作者。
(一)引言
在当今数字化时代,敏感数据的保护至关重要,而用户认证则是守护这些数据的第一道防线。用户认证能够确认用户的身份,确保只有合法授权的用户才能访问敏感信息,从而有效防止数据泄露和滥用。HarmonyOS Next针对用户认证场景,提供了一套全面且强大的关键资产保护方案,旨在为用户的敏感数据提供最高级别的安全保障。
(二)需要用户认证的场景
- 金融类应用查看银行卡号场景流程
- 以用户在金融类应用中查看银行卡号为例,其流程通常如下:首先,用户打开金融应用并导航至查看银行卡号的功能界面。此时,应用会检测到用户正在尝试访问敏感的关键资产(银行卡号),于是触发用户认证流程。应用向系统发送认证请求,系统根据应用的设置和用户的设备情况,决定采用何种认证方式(如指纹识别、人脸认证或PIN码输入等)。假设应用设置为优先使用指纹识别,用户将手指放置在设备的指纹传感器上,系统采集指纹信息并与预先存储的用户指纹数据进行比对。如果指纹匹配成功,用户认证通过,应用获得授权查询并向用户展示银行卡号。若认证失败,例如指纹不匹配或用户多次认证错误,应用会根据安全策略采取相应措施,如限制访问次数、提示用户重新认证或引导用户使用其他认证方式。 - 用户认证对保障资产安全的重要性
- 在这个场景中,用户认证起着至关重要的作用。银行卡号属于用户的敏感金融信息,一旦泄露,可能导致资金被盗取等严重后果。通过用户认证,确保只有银行卡的持卡人本人能够查看卡号,有效防止了他人非法获取卡号信息,保护了用户的财产安全。即使设备丢失或被盗,如果没有通过用户认证,XX者也无法轻易获取银行卡号,为用户的资金安全提供了有力保障。
(三)关键资产属性与设置
- 特殊属性列表
- 在需要用户认证的场景下,关键资产具有一些特殊属性,其中最重要的是AUTH_TYPE(认证类型)属性。该属性用于指定访问关键资产所需的用户认证方式,例如可以取值为userAuth.UserAuthType.PIN(表示PIN码认证)、userAuth.UserAuthType.FINGERPRINT(指纹认证)、userAuth.UserAuthType.FACE(人脸认证)等。此外,还有AUTH_VALIDITY_PERIOD(认证有效期)属性,取值范围为1 - 600秒,用于设定用户认证通过后,在多长时间内可以无需再次认证而访问相关关键资产。例如,设置为60秒,意味着用户在成功认证后的60秒内再次访问同一关键资产时,无需重复认证过程,提高了用户体验的同时也在一定程度上平衡了安全性。 - 属性设置实现安全访问控制的方式
- 开发者在设置这些属性时,需要根据应用的安全需求和用户体验进行权衡。对于高度敏感的关键资产,如银行卡号,可能会选择多种认证方式的组合,如同时启用指纹认证和PIN码认证,以增加认证的安全性。例如,以下是一段设置关键资产属性以实现安全访问控制的示例代码(ArkTS语言):
let attr: asset.AssetMap = new Map();
attr.set(asset.Tag.SECRET, stringToArray('bankCardNumber123')); // 假设银行卡号为"bankCardNumber123"
attr.set(asset.Tag.ALIAS, stringToArray('bankCardAlias')); // 设置别名为"bankCardAlias"
attr.set(asset.Tag.AUTH_TYPE, userAuth.UserAuthType.FINGERPRINT | userAuth.UserAuthType.PIN); // 设置认证类型为指纹和PIN码认证
attr.set(asset.Tag.AUTH_VALIDITY_PERIOD, 300); // 设置认证有效期为300秒
通过这样的设置,只有在用户通过指纹和PIN码认证后,且在300秒内,才能访问别名为“bankCardAlias”的关键资产(即银行卡号),从而实现了严格的安全访问控制。
(四)查询认证关键资产流程
- 详细步骤
- 预处理:应用首先构建查询条件,通过设置关键资产的属性(如ALIAS别名等)来确定要查询的目标关键资产。然后,调用preQuery
接口发起预处理请求。系统接收到请求后,会根据查询条件进行初步处理,并生成一个用户认证挑战值(AUTH_CHALLENGE)。例如,以下是预处理的代码示例:
async function preQueryAsset(): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
try {
let query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, stringToArray('bankCardAlias')); // 设置要查询的关键资产别名
asset.preQuery(query).then((challenge: Uint8Array) => {
resolve(challenge);
}).catch(() => {
reject();
});
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to pre-query Asset. Code is ${err.code}, message is ${err.message}`);
reject();
}
});
}
- 用户认证:应用接收到挑战值后,调用系统的用户认证接口(如userAuth.getUserAuthInstance
),并将挑战值传递给认证接口。系统根据认证类型(如之前设置的指纹和PIN码认证),弹出相应的认证界面(如指纹识别界面或PIN码输入界面),提示用户进行认证。用户完成认证操作后,认证接口会返回认证结果(包括认证成功与否以及认证令牌AUTH_TOKEN等信息)。以下是用户认证的代码示例:
async function userAuthenticate(challenge: Uint8Array): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const authParam: userAuth.AuthParam = {
challenge: challenge,
authType: [userAuth.UserAuthType.FINGERPRINT, userAuth.UserAuthType.PIN], // 设置认证类型
authTrustLevel: userAuth.AuthTrustLevel.ATL1,
};
const widgetParam: userAuth.WidgetParam = { title: '请输入锁屏密码或进行指纹认证' };
try {
let userAuthInstance = userAuth.getUserAuthInstance(authParam, widgetParam);
userAuthInstance.on('result', {
onResult(result) {
if (result.result == userAuth.UserAuthResultCode.SUCCESS) {
console.info(`User identity authentication succeeded.`);
resolve(result.token);
} else {
console.error(`User identity authentication failed.`);
reject();
}
}
});
userAuthInstance.start();
} catch (error) {
let err = error as BusinessError;
console.error(`User identity authentication failed. Code is ${err.code}, message is ${err.message}`);
reject();
}
});
}
- 查询明文:用户认证成功后,应用将认证令牌(AUTH_TOKEN)和挑战值(AUTH_CHALLENGE)以及其他必要的查询参数(如ALIAS别名等)一起,通过query
接口发起查询明文请求。系统验证认证令牌和挑战值的有效性后,如果验证通过,会根据查询条件从安全存储中获取关键资产的明文数据,并返回给应用。以下是查询明文的代码示例:
async function queryAsset() {
preQueryAsset().then(async (challenge: Uint8Array) => {
try {
let authToken: Uint8Array = await userAuthenticate(challenge);
let query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, stringToArray('bankCardAlias'));
query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);
query.set(asset.Tag.AUTH_CHALLENGE, challenge);
query.set(asset.Tag.AUTH_TOKEN, authToken);
let res: Array<asset.AssetMap> = await asset.query(query);
for (let i = 0; i < res.length; i++) {
// 处理查询到的明文数据
let secret: Uint8Array = res[i].get(asset.Tag.SECRET) as Uint8Array;
let secretStr: string = arrayToString(secret);
console.log('Bank card number:', secretStr);
}
} catch (error) {
// 处理认证或查询失败的情况
}
}).catch((err: BusinessError) => {
console.error(`Failed to pre-query Asset. Code is ${err.code}, message is ${err.message}`);
});
}
- 后置处理:查询明文成功后,应用需要调用postQuery
接口进行后置处理,通知系统查询操作已完成。这一步骤有助于系统进行资源管理和状态更新等操作。以下是后置处理的代码示例:
async function postQueryAsset(challenge: Uint8Array) {
let handle: asset.AssetMap = new Map();
handle.set(asset.Tag.AUTH_CHALLENGE, challenge);
try {
await asset.postQuery(handle);
console.info(`Succeeded in post-querying Asset.`);
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to post-query Asset. Code is ${err.code}, message is ${err.message}`);
}
}
(五)代码示例与解析
- 完整代码示例
import { asset } from '@kit.AssetStoreKit';
import { util } from '@kit.ArkTS';
import userAuth from '@ohos.userIAM.userAuth';
import { BusinessError } from '@kit.BasicServicesKit';
function stringToArray(str: string): Uint8Array {
let textEncoder = new util.TextEncoder();
return textEncoder.encodeInto(str);
}
function arrayToString(arr: Uint8Array): string {
let textDecoder = util.TextDecoder.create("utf-8", { ignoreBOM: true });
let str = textDecoder.decodeWithStream(arr, { stream: false });
return str;
}
// 预处理
async function preQueryAsset(): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
try {
let query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, stringToArray('bankCardAlias'));
asset.preQuery(query).then((challenge: Uint8Array) => {
resolve(challenge);
}).catch(() => {
reject();
});
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to pre-query Asset. Code is ${err.code}, message is ${err.message}`);
reject();
}
});
}
// 用户认证
async function userAuthenticate(challenge: Uint8Array): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const authParam: userAuth.AuthParam = {
challenge: challenge,
authType: [userAuth.UserAuthType.FINGERPRINT, userAuth.UserAuthType.PIN],
authTrustLevel: userAuth.AuthTrustLevel.ATL1,
};
const widgetParam: userAuth.WidgetParam = { title: '请输入锁屏密码或进行指纹认证' };
try {
let userAuthInstance = userAuth.getUserAuthInstance(authParam, widgetParam);
userAuthInstance.on('result', {
onResult(result) {
if (result.result == userAuth.UserAuthResultCode.SUCCESS) {
console.info(`User identity authentication succeeded.`);
resolve(result.token);
} else {
console.error(`User identity authentication failed.`);
reject();
}
}
});
userAuthInstance.start();
} catch (error) {
let err = error as BusinessError;
console.error(`User identity authentication failed. Code is ${err.code}, message is ${err.message}`);
reject();
}
});
}
// 查询明文
async function queryAsset() {
preQueryAsset().then(async (challenge: Uint8Array) => {
try {
let authToken: Uint8Array = await userAuthenticate(challenge);
let query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, stringToArray('bankCardAlias'));
query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);
query.set(asset.Tag.AUTH_CHALLENGE, challenge);
query.set(asset.Tag.AUTH_TOKEN, authToken);
let res: Array<asset.AssetMap> = await asset.query(query);
for (let i = 0; i < res.length; i++) {
let secret: Uint8Array = res[i].get(asset.Tag.SECRET) as Uint8Array;
let secretStr: string = arrayToString(secret);
console.log('Bank card number:', secretStr);
}
// 后置处理
postQueryAsset(challenge);
} catch (error) {
// 处理认证或查询失败的情况
postQueryAsset(challenge);
}
}).catch((err: BusinessError) => {
console.error(`Failed to pre-query Asset. Code is ${err.code}, message is ${err.message}`);
});
}
// 后置处理
async function postQueryAsset(challenge: Uint8Array) {
let handle: asset.AssetMap = new Map();
handle.set(asset.Tag.AUTH_CHALLENGE, challenge);
try {
await asset.postQuery(handle);
console.info(`Succeeded in post-querying Asset.`);
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to post-query Asset. Code is ${err.code}, message is ${err.message}`);
}
}
- 代码解析
- 首先,preQueryAsset
函数用于发起预处理请求,通过设置关键资产的别名,向系统查询并获取用户认证挑战值。如果预处理成功,将挑战值传递给userAuthenticate
函数进行用户认证。
-userAuthenticate
函数接收挑战值后,构建认证参数(包括挑战值、认证类型和信任级别等),然后调用系统的用户认证接口,弹出认证界面等待用户操作。如果认证成功,返回认证令牌;如果失败,拒绝并输出错误信息。
-queryAsset
函数是整个流程的核心,它先调用preQueryAsset
获取挑战值,然后使用挑战值调用userAuthenticate
进行用户认证,认证成功后,构建查询参数(包括别名、返回类型、挑战值和认证令牌等),向系统查询关键资产的明文数据。如果查询成功,处理并输出明文数据,最后调用postQueryAsset
进行后置处理。如果在任何一个环节出现错误,都会调用postQueryAsset
进行后置处理,以确保系统状态的正确更新。
-postQueryAsset
函数用于完成后置处理,通知系统查询操作已完成,释放相关资源。
(六)总结与应用拓展
- 关键资产保护要点总结
- 在用户认证场景下的关键资产保护中,关键要点包括合理设置关键资产的属性,特别是认证类型和认证有效期等属性,根据资产的敏感程度选择合适的认证方式组合。严格按照查询认证关键资产的流程进行操作,确保预处理、用户认证、查询明文和后置处理等环节的正确性和完整性。在代码实现过程中,要正确处理各种错误情况,保证系统的稳定性和安全性。同时,要充分利用HarmonyOS Next提供的安全机制,如可信执行环境等,保护用户认证过程和关键资产数据的安全。 - 其他类似场景应用可能性探讨
- 除了金融类应用查看银行卡号场景外,这种用户认证场景下的关键资产保护方案还可以广泛应用于其他类似场景。例如,在企业办公应用中,员工查看公司机密文件(关键资产)时,可以采用类似的用户认证流程,确保只有授权员工能够访问敏感文件。在医疗健康应用中,患者查看个人病历和医疗数据时,也可以通过用户认证来保护隐私。此外,在智能家居系统中,用户控制和查看家庭安防设备的录像(关键资产)时,同样可以利用用户认证机制,防止未经授权的访问,保障家庭安全和隐私。通过灵活运用HarmonyOS Next的用户认证和关键资产保护方案,可以为各种应用场景提供强大的安全保障,满足不同用户对数据安全的需求。