【HarmonyOS NEXT】获取卸载APP后不变的设备ID

harmonyos,next,app,id · 浏览次数 : 11

小编点评

**类名:DeviceUtils** **函数:** * **get():**根据索引获取数据。 * **getAssetMap():**根据索引获取资产地图。 * **updateAssetMap():**根据索引更新资产地图。 **私有方法:** * **deviceIdCacheKey:**缓存设备ID的 key。 * **deviceId:**存储设备ID的变量。 **主要功能:** * 获取设备ID,并从缓存中获取或随机生成。 * 从资产地图中获取资产数据。 * 更新资产地图,并从缓存中获取或设置值。 **使用方式:** 1. 创建一个 DeviceUtils 对象。 2. 调用 `getDeviceId()` 方法获取设备ID。 3. 调用 `getAssetMap()` 方法获取资产地图。 4. 调用 `updateAssetMap()` 方法更新资产地图。 **依赖:** * ohpm i @ranran/utilcode (用于获取设备ID) **注意:** * 为了使用 `store_persistent_data` 权限,需要在 `requestPermissions` 中添加 `ohos.permission.STORE_PERSISTENT_DATA`。 * 由于代码集成到项目中可能比较复杂,可以使用远程依赖的方式引入 `utilcode` 库。

正文

1. 背景

在HarmonyOS NEXT中,想要获取设备ID,有3种方式

UDID: deviceinfo.udid ,仅限系统应用使用

AAID: aaid.getAAID(),然而卸载APP/恢复设备出厂设置/后会发生变化

OAID:identifier.getOAID,同一台设备上不同的App获取到的OAID值一样,但是用户如果关闭跟踪开关,该应用仅能获取到全0的OAID。且使用该API,需要申请申请广告跟踪权限ohos.permission.APP_TRACKING_CONSENT,触发动态授权弹框,向用户请求授权,用户授权成功后才可获取。

2. 问题

从上述三种方法中我们发现,无法实现 不需要申请动态权限,且App卸载后不变的设备ID。但是天无绝人之路,有一种取巧的办法可以实现。下面是具体办法。

3. 解决办法

在HarmonyOS NEXT中,有一个 @ohos.security.asset (关键资产存储服务) 的API【类似于iOS中的Keychain services】,有一个特殊属性 IS_PERSISTENT,该特性可实现,在应用卸载时保留关键资产,利用该特性,我们可以随机生成一个32位的uuid,存储到ohos.security.asset中。

4. 源码实现

4.1. 封装AssetStore

import { asset } from '@kit.AssetStoreKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

/// AssetStore 操作结果
export interface AssetStoreResult {
  isSuccess: boolean;
  error?: BusinessError;
  data?: string;
}

/// AssetStore query 操作结果
export interface AssetStoreQueryResult {
  res?: asset.AssetMap[];
  error?: BusinessError;
}

/**
 * 基于 @ohos.security.asset 的封装。可以保证『重装/删除应用而不丢失数据』。
 * @author Tanranran
 * @date 2024/5/14 22:14
 * @description
 * 关键资产存储服务提供了用户短敏感数据的安全存储及管理能力。
 * 其中,短敏感数据可以是密码类(账号/密码)、Token类(应用凭据)、其他关键明文(如银行卡号)等长度较短的用户敏感数据。
 * 可在应用卸载时保留数据。需要权限: ohos.permission.STORE_PERSISTENT_DATA。
 * 更多API可参考https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-asset-0000001815758836-V5
 * 使用例子:
 * // 增。
 const result = await AssetStore.set('key', 'value');
 if (result.isSuccess) {
 console.log('asset add succeeded')
 }

 // 删。
 AssetStore.remove('key');
 if (result.isSuccess) {
 console.log('asset remove succeeded')
 }

 // 改
 const result = await AssetStore.update('key', 'value');
 if (result.isSuccess) {
 console.log('asset update succeeded')
 }

 // 读取。
 const result = (await AssetStore.get('key'));
 if (result.isSuccess) {
 console.log('asset get succeeded, value == ', result.data)
 }
 */
export class AssetStore {
  /**
   * 新增数据
   * 添加成功,会通过 AppStorage 传值值变更,外部可通过 @StorageProp(key) value: string 观察值变化。
   * @param key           要添加的索引
   * @param value         要添加的值
   * @param isPersistent  在应用卸载时是否需要保留关键资产,默认为 true
   * @returns Promise<AssetStoreResult> 表示添加操作的异步结果
   */
  public static async set(key: string, value: string, isPersistent: boolean = true): Promise<AssetStoreResult> {
    let attr: asset.AssetMap = new Map();
    if (canIUse("SystemCapability.Security.Asset")) {
      // 关键资产别名,每条关键资产的唯一索引。
      // 类型为Uint8Array,长度为1-256字节。
      attr.set(asset.Tag.ALIAS, AssetStore.stringToArray(key));
      // 关键资产明文。
      // 类型为Uint8Array,长度为1-1024字节
      attr.set(asset.Tag.SECRET, AssetStore.stringToArray(value));

      // 关键资产同步类型>THIS_DEVICE只在本设备进行同步,如仅在本设备还原的备份场景。
      attr.set(asset.Tag.SYNC_TYPE, asset.SyncType.THIS_DEVICE);

      //枚举,新增关键资产时的冲突(如:别名相同)处理策略。OVERWRITE》抛出异常,由业务进行后续处理。
      attr.set(asset.Tag.CONFLICT_RESOLUTION,asset.ConflictResolution.THROW_ERROR)
      // 在应用卸载时是否需要保留关键资产。
      // 需要权限: ohos.permission.STORE_PERSISTENT_DATA。
      // 类型为bool。
      if (isPersistent) {
        attr.set(asset.Tag.IS_PERSISTENT, isPersistent);
      }
    }
    let result: AssetStoreResult
    if ((await AssetStore.has(key)).isSuccess) {
      result = await AssetStore.updateAssetMap(attr, attr);
    } else {
      result = await AssetStore.setAssetMap(attr);
    }
    if (result.isSuccess) {
      hilog.debug(0x1111,'AssetStore',
        `AssetStore: Asset add succeeded. Key is ${key}, value is ${value}, isPersistent is ${isPersistent}`);
      // 添加成功,会通过 AppStorage 传值值变更,外部可通过 @StorageProp(key) value: string 观察值变化。
      AppStorage.setOrCreate(key, value);
    }
    return result;
  }

  /**
   * 新增数据
   * @param attr          要添加的属性集
   * @returns Promise<AssetStoreResult> 表示添加操作的异步结果
   */
  public static async setAssetMap(attr: asset.AssetMap): Promise<AssetStoreResult> {
    try {
      if (canIUse("SystemCapability.Security.Asset")) {
        await asset.add(attr);
        return { isSuccess: true };
      }
      return { isSuccess: false, error: AssetStore.getUnSupportedPlatforms() };
    } catch (error) {
      const err = error as BusinessError;
      hilog.debug(0x1111,'AssetStore',
        `AssetStore: Failed to add Asset. Code is ${err.code}, message is ${err.message}`);
      return { isSuccess: false, error: err };
    }
  }

  /**
   * 删除数据
   * 删除成功,会通过 AppStorage 传值值变更,外部可通过 @StorageProp(key) value: string 观察值变化。
   * AppStorage API12 及以上支持 undefined 和 null类型。
   * @param key           要删除的索引
   * @returns Promise<AssetStoreResult> 表示添加操作的异步结果
   */
  public static async remove(key: string) {
    let query: asset.AssetMap = new Map();
    if (canIUse("SystemCapability.Security.Asset")) {
      // 关键资产别名,每条关键资产的唯一索引。
      // 类型为Uint8Array,长度为1-256字节。
      query.set(asset.Tag.ALIAS, AssetStore.stringToArray(key));
    }
    const result = await AssetStore.removeAssetMap(query);
    if (result.isSuccess) {
      hilog.debug(0x1111,'AssetStore', `AssetStore: Asset remove succeeded. Key is ${key}`);
      // 删除成功,会通过 AppStorage 传值值变更,外部可通过 @StorageProp(key) value: string 观察值变化。
      // AppStorage API12 及以上支持 undefined 和 null类型。
      AppStorage.setOrCreate(key, '');
    }
    return result;
  }

  /**
   * 删除数据
   * @param attr          要删除的属性集
   * @returns Promise<AssetStoreResult> 表示添加操作的异步结果
   */
  public static async removeAssetMap(attr: asset.AssetMap): Promise<AssetStoreResult> {
    try {
      if (canIUse("SystemCapability.Security.Asset")) {
        await asset.remove(attr);
        return { isSuccess: true };
      }
      return { isSuccess: false };
    } catch (error) {
      const err = error as BusinessError;
      hilog.debug(0x1111,'AssetStore',
        `AssetStore: Failed to remove Asset. Code is ${err.code}, message is ${err.message}`);
      return { isSuccess: false, error: err };
    }
  }

  /**
   * 判断是否存在 数据
   * @param key 要查找的索引
   * @returns Promise<AssetStoreResult> 表示添加操作的异步结果
   */
  public static async has(key: string): Promise<AssetStoreResult> {
    if (canIUse("SystemCapability.Security.Asset")) {
      let query: asset.AssetMap = new Map();
      // 关键资产别名,每条关键资产的唯一索引。
      // 类型为Uint8Array,长度为1-256字节。
      query.set(asset.Tag.ALIAS, AssetStore.stringToArray(key));
      // 	关键资产查询返回的结果类型。
      query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);

      const result = await AssetStore.getAssetMap(query);

      const res = result.res;
      if (!res) {
        return { isSuccess: false, error: result.error };
      }
      if (res.length < 1) {
        return { isSuccess: false };
      }
    }
    return { isSuccess: false };
  }

  /**
   * 查找数据
   * @param key          要查找的索引
   * @returns Promise<AssetStoreResult> 表示添加操作的异步结果
   */
  public static async get(key: string): Promise<AssetStoreResult> {
    if (canIUse("SystemCapability.Security.Asset")) {
      let query: asset.AssetMap = new Map();
      // 关键资产别名,每条关键资产的唯一索引。
      // 类型为Uint8Array,长度为1-256字节。
      query.set(asset.Tag.ALIAS, AssetStore.stringToArray(key));
      // 	关键资产查询返回的结果类型。
      query.set(asset.Tag.RETURN_TYPE, asset.ReturnType.ALL);

      const result = await AssetStore.getAssetMap(query);

      const res = result.res;
      if (!res) {
        return { isSuccess: false, error: result.error };
      }
      if (res.length < 1) {
        return { isSuccess: false };
      }
      // parse the secret.
      let secret: Uint8Array = res[0].get(asset.Tag.SECRET) as Uint8Array;
      // parse uint8array to string
      let secretStr: string = AssetStore.arrayToString(secret);
      return { isSuccess: true, data: secretStr };
    }
    return { isSuccess: false, data: "" };
  }

  /**
   * 查找数据
   * @param key          要查找的索引
   * @returns Promise<AssetStoreQueryResult> 表示添加操作的异步结果
   */
  public static async getAssetMap(query: asset.AssetMap): Promise<AssetStoreQueryResult> {
    try {
      if (canIUse("SystemCapability.Security.Asset")) {
        const res: asset.AssetMap[] = await asset.query(query);
        return { res: res };
      }
      return { error: AssetStore.getUnSupportedPlatforms() };
    } catch (error) {
      const err = error as BusinessError;
      hilog.debug(0x1111,'AssetStore',
        `AssetStore>getAssetMap: Failed to query Asset. Code is ${err.code}, message is ${err.message}`);
      return { error: err };
    }
  }


  /**
   * 更新数据
   * @param query           要更新的索引数据集
   * @param attrsToUpdate   要更新的数据集
   * @returns Promise<AssetStoreResult> 表示添加操作的异步结果
   */
  public static async updateAssetMap(query: asset.AssetMap, attrsToUpdate: asset.AssetMap): Promise<AssetStoreResult> {
    try {
      if (canIUse("SystemCapability.Security.Asset")) {
        await asset.update(query, attrsToUpdate);
        return { isSuccess: true };
      }
      return { isSuccess: false, error: AssetStore.getUnSupportedPlatforms() };
    } catch (error) {
      const err = error as BusinessError;
      hilog.debug(0x1111, 'AssetStore',
        `AssetStore: Failed to update Asset. Code is ${err.code}, message is ${err.message}`);
      return { isSuccess: false, error: err };
    }
  }

  private static stringToArray(str: string): Uint8Array {
    let textEncoder = new util.TextEncoder();
    return textEncoder.encodeInto(str);
  }

  private static arrayToString(arr: Uint8Array): string {
    let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true });
    let str = textDecoder.decodeWithStream(arr, { stream: false });
    return str;
  }

  private static getUnSupportedPlatforms() {
    return { name: "AssetStore", message: "不支持该平台" } as BusinessError
  }
}

4.2. 封装DeviceUtils

/**
 * @author Tanranran
 * @date 2024/5/14 22:20
 * @description
 */
import { AssetStore } from './AssetStore'
import { util } from '@kit.ArkTS'

export class DeviceUtils {
  private static deviceIdCacheKey = "device_id_cache_key"
  private static deviceId = ""

  /**
   * 获取设备id>32为随机码[卸载APP后依旧不变]
   * @param isMD5
   * @returns
   */
  static async getDeviceId() {
    let deviceId = DeviceUtils.deviceId
    //如果内存缓存为空,则从AssetStore中读取
    if (!deviceId) {
      deviceId = `${(await AssetStore.get(DeviceUtils.deviceIdCacheKey)).data}`
    }
    //如果AssetStore中未读取到,则随机生成32位随机码,然后缓存到AssetStore中
    if (deviceId) {
      deviceId = util.generateRandomUUID(true).replace('-', '')
      AssetStore.set(DeviceUtils.deviceIdCacheKey, deviceId)
    }
    DeviceUtils.deviceId = deviceId
    return deviceId
  }
}

4.3. 使用

1、module.json5 中requestPermissions里增加ohos.permission.STORE_PERSISTENT_DATA 权限【只需要声明即可,不需要动态申请】

2、

import { DeviceUtils } from './DeviceUtils';
console.log(await DeviceUtils.getDeviceId())

5. 远程依赖

如果觉得上述源码方式集成到项目中比较麻烦,可以使用远程依赖的方式引入

通过 ohpm 安装utilcode库。

ohpm i @ranran/utilcode

使用

import { DeviceUtils } from '@ranran/utilcode';
console.log(await DeviceUtils.getDeviceId())

本文正在参加华为鸿蒙有奖征文征文活动

与【HarmonyOS NEXT】获取卸载APP后不变的设备ID相似的内容:

【HarmonyOS NEXT】获取卸载APP后不变的设备ID

1. 背景 在HarmonyOS NEXT中,想要获取设备ID,有3种方式 UDID: deviceinfo.udid ,仅限系统应用使用 AAID: aaid.getAAID(),然而卸载APP/恢复设备出厂设置/后会发生变化 OAID:identifier.getOAID,同一台设备上不同的Ap

HarmonyOS 实战开发-Worker子线程中解压文件

本示例介绍在Worker子线程使用@ohos.zlib提供的zlib.decompressfile接口对沙箱目录中的压缩文件进行解压操作,解压成功后将解压路径返回主线程,获取解压文件列表。

日志服务 HarmonyOS NEXT 日志采集最佳实践

背景信息 随着数字化新时代的全面展开以及 5G 与物联网(IoT)技术的迅速普及,操作系统正面临前所未有的变革需求。在这个背景下,华为公司自主研发的鸿蒙操作系统(HarmonyOS)应运而生,旨在满足万物互联时代的多元化设备接入、高效协同和安全可靠运行的需求。 HarmonyOS 不仅着眼于智能手机

解锁HDC 2024之旅:从购票到报名,全程攻略

本文分享自华为云社区《解锁HDC 2024之旅:从购票到报名,全程攻略》,作者:华为云社区精选。 Hi,代码界的小伙伴们,集结号已经吹响了!华为开发者大会(HDC 2024)——这场汇聚了HarmonyOS NEXT鸿蒙星河版、盘古大模型5.0等创新火花与智慧碰撞的盛宴,将于6月21日至23日在东莞

鸿蒙HarmonyOS实战-窗口管理

前言 窗口管理是指计算机操作系统中管理和控制窗口的一种机制。窗口管理器负责处理窗口的创建、关闭、移动、调整大小等操作,并且决定窗口的位置、层级、是否可见、是否接收用户输入等属性。窗口管理器还负责绘制窗口的外观和边框,并提供用户与窗口交互的方式,如鼠标点击、键盘输入等。窗口管理器可以通过图形用户界

HarmonyOS 鸿蒙隔离层设计

在软件开发中,底层库的更换或升级是常见的需求,这可能由性能提升、新功能需求或安全性考虑等因素驱动。为了降低迁移成本,良好的设计模式至关重要。 在版本迭代过程中,网络请求库可能会经历从A到B再到C的演进。为了实现业务层的无感切换,需要在各个请求库和业务代码之间封装隔离代码,以实现第三方网络库的灵活更换

HarmonyOS 应用生命周期有哪些? 按返回键会调用哪些生命周期?

UIAbility 生命周期: onCreate :页面初始化,变量定义,资源加载。 onWindowStageCreate:设置 UI 界面加载、设置 WindowStage 的事件订阅。 onForeground:切换至前台,申请系统需要的资源,或者重新申请在 onBackground()中释放

App如何利用推送消息有效实现拉新促活?

对于大多数App来说,如何快速建立与用户的联系、提高用户活跃度、提升用户转化率,是产品运营过程中十分关心的问题,在常见的运营手段中,Push推送消息以其高性价比成为首选策略。但在实际运营过程中,推送消息的打开率和转化率远远达不到预期,App日活难以提升。那么如何才能有效提高打开和转化率,快速实现Ap

HiAI Foundation开发平台,加速端侧AI应用的智能革命

如果您是一名开发者,正在寻找一种高效、灵活且易于使用的端侧AI开发框架,那么HarmonyOS SDKHiAI Foundation服务(HiAI Foundation Kit)就是您的理想选择。 作为一款AI开发框架,HiAI Foundation不仅提供强大的NPU计算能力和丰富的开发工具,还提

解决卡顿发热,超帧技术焕发中重载游戏动力

近几年,中国手游市场规模不断扩大,开发者通过在画面、玩法等方面的持续创新和打磨,推出更加精品化的产品。然而愈发精美的画质和复杂的玩法,也给硬件带来超高的负载,导致玩家在游戏过程中,频繁出现掉帧卡顿、发烫、续航差等体验降低的现象。 HarmonyOS SDK 图形加速服务(Graphics Accel