在HarmonyOS应用开发中,设备唯一标识符(Device Unique Identifier)是确保用户账户安全和个性化体验的重要组成部分。HarmonyOS NEXT 提供了多种方式来获取设备标识符,包括OAID(Open Anonymous ID)和AAID(Android Advertising ID)。然而,这些标识符在某些情况下可能不够稳定,例如当用户重置设备或卸载应用后,这些标识符可能会发生变化。为了提供更加稳定的设备标识符,HarmonyOS NEXT 引入了 Asset Store Kit(关键资源存储开发服务),它允许开发者在设备上安全地存储和管理关键资源,如账号信息、密码等。
本文将详细介绍如何使用 Asset Store Kit 实现设备唯一ID的持久化存储,确保用户在卸载并重装应用后仍然能够保持原有的账户信息。
场景描述
假设我们有一个应用,用户在首次登录时需要输入账号和密码。一旦登录成功,即使用户卸载并重装应用,再次打开应用时也能够自动恢复之前的登录状态,无需重新输入账号和密码。
方案描述
1. 配置权限
首先,我们需要在 module.json5
文件中配置 ohos.permission.STORE_PERSISTENT_DATA
权限,以便使用 Asset Store Kit。
{
"module": {
"abilities": [
{
"name": ".MainAbility",
"label": "$string:app_name",
"icon": "$media:icon",
"description": "$string:app_desc",
"type": "page",
"launchType": "standard",
"orientation": "unspecified",
"skills": [
{
"actions": ["action.system.home"]
}
]
}
],
"reqPermissions": [
"ohos.permission.STORE_PERSISTENT_DATA"
]
}
}
2. 实现持久化存储
2.1 查询存储的数据
在应用启动时,我们需要检查是否存在已经存储的账号信息。如果存在,则直接使用这些信息;如果不存在,则提示用户进行登录。
async aboutToAppear(): Promise<void> {
let query: asset.AssetMap = new Map();
query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ATTRIBUTES); // 返回关键资源属性,不含关键资源明文。
try {
let res = asset.querySync(query);
if (res.length > 0) {
let alias = res[0].get(asset.Tag.ALIAS) as Uint8Array;
let aliasStr = arrayToString(alias);
let query2: asset.AssetMap = new Map();
query2.set(asset.Tag.ALIAS, stringToArray(aliasStr));
query2.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL); // 返回关键资源明文及属性
let res2 = asset.querySync(query2);
for (let i = 0; i < res2.length; i++) {
let ID = res2[i].get(asset.Tag.SECRET) as Uint8Array;
let IDStr = arrayToString(ID);
let deviceType = res2[i].get(asset.Tag.DATA_LABEL_NORMAL_1) as Uint8Array;
let deviceTypeStr = arrayToString(deviceType);
// 存储查询到的账号信息
let myList: MesList = new MesList(aliasStr, IDStr, deviceTypeStr);
router.pushUrl({
url: 'pages/Search_AssetLogin',
params: { myList: myList }
});
}
} else {
console.log('暂无设备');
}
} catch (err) {
console.error('查询失败', err);
}
}
2.2 用户登录
当用户首次登录时,我们需要将账号信息存储到 Asset Store Kit 中。如果用户已经登录过,则直接使用存储的信息。
async function login(account: string, password: string) {
let deviceTypeStr = deviceInfo.marketName;
let query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, stringToArray(account)); // 指定了关键资源别名,最多查询到一条满足条件的关键资源
query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL); // 此处表示需要返回关键资源的所有信息,即属性+明文
await asset.query(query).then((res: Array<asset.AssetMap>) => {
if (res.length > 0) {
for (let i = 0; i < res.length; i++) {
let inputAccount = res[i].get(asset.Tag.ALIAS) as Uint8Array;
let inputAccountStr = arrayToString(inputAccount);
let ID = res[i].get(asset.Tag.SECRET) as Uint8Array;
let IDStr = arrayToString(ID);
let deviceType = res[i].get(asset.Tag.DATA_LABEL_NORMAL_1) as Uint8Array;
let deviceTypeStr = arrayToString(deviceType);
if (account === inputAccountStr && password === '123456') {
let myList: MesList = new MesList(inputAccountStr, IDStr, deviceTypeStr);
router.pushUrl({
url: 'pages/Search_AssetLogin',
params: { myList: myList }
});
} else {
console.log('密码错误');
}
}
} else {
// 新设备,弹窗提示用户信任
AlertDialog.show({
title: '是否信任此设备',
subtitle: '',
message: '',
autoCancel: true,
alignment: DialogAlignment.Bottom,
gridCount: 4,
offset: { dx: 0, dy: -20 },
primaryButton: {
value: '取消',
action: () => {
console.log('请重新登录');
}
},
secondaryButton: {
enabled: true,
defaultFocus: true,
style: DialogButtonStyle.HIGHLIGHT,
value: '确认',
action: () => {
let attr: asset.AssetMap = new Map();
attr.set(asset.Tag.ALIAS, stringToArray(account));
attr.set(asset.Tag.SECRET, stringToArray(state.ID));
attr.set(asset.Tag.DATA_LABEL_NORMAL_1, stringToArray(deviceTypeStr));
attr.set(asset.Tag.IS_PERSISTENT, true);
try {
asset.addSync(attr); // 第一次登录,弹窗点击信任添加数据
console.log("登录成功");
let query: asset.AssetMap = new Map();
query.set(asset.Tag.ALIAS, stringToArray(account)); // 指定了关键资源别名,最多查询到一条满足条件的关键资源
query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL); // 此处表示需要返回关键资源的所有信息,即属性+明文
let res = asset.querySync(query);
for (let i = 0; i < res.length; i++) {
let inputAccount = res[i].get(asset.Tag.ALIAS) as Uint8Array;
let inputAccountStr = arrayToString(inputAccount);
let ID = res[i].get(asset.Tag.SECRET) as Uint8Array;
let IDStr = arrayToString(ID);
let deviceType = res[i].get(asset.Tag.DATA_LABEL_NORMAL_1) as Uint8Array;
let deviceTypeStr = arrayToString(deviceType);
let myList: MesList = new MesList(inputAccountStr, IDStr, deviceTypeStr);
router.pushUrl({
url: 'pages/Search_AssetLogin',
params: { myList: myList }
});
}
} catch (error) {
if (error.code === 24000003) {
console.log('请勿重复登录');
} else {
console.log('登录失败');
}
}
}
}
});
}
}).catch((err: BusinessError) => {
console.error('查询失败', err);
});
}
2.3 删除存储的资源
在某些情况下,用户可能希望注销账户或删除存储的资源。我们可以通过 remove
方法来实现这一点。
export class One {
async remove() {
let query: asset.AssetMap = new Map();
try {
asset.remove(query).then(() => {
console.info(`Asset removed successfully.`);
router.pushUrl({
url: 'pages/Asset_login'
});
console.log('请重新登录');
}).catch((err: BusinessError) => {
console.error(`Failed to remove Asset. Code is ${err.code}, message is ${err.message}`);
});
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to remove Asset. Code is ${err.code}, message is ${err.message}`);
}
}
}
辅助函数
为了方便处理字符串和 Uint8Array 之间的转换,我们可以定义一些辅助函数。
function arrayToString(array: Uint8Array): string {
return new TextDecoder().decode(array);
}
function stringToArray(str: string): Uint8Array {
return new TextEncoder().encode(str);
}