在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。

HarmonyOS开发之设备唯一ID方案_唯一标识

{
  "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"
    ]
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
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);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
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);
  });
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
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}`);
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
辅助函数

为了方便处理字符串和 Uint8Array 之间的转换,我们可以定义一些辅助函数。

function arrayToString(array: Uint8Array): string {
  return new TextDecoder().decode(array);
}

function stringToArray(str: string): Uint8Array {
  return new TextEncoder().encode(str);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.