HarmonyOS 鸿蒙Next获取卸载APP后不变的设备ID

发布于 1周前 作者 yibo5220 来自 鸿蒙OS

HarmonyOS 鸿蒙Next获取卸载APP后不变的设备ID
<markdown _ngcontent-nup-c237="" class="markdownPreContainer">

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](/user/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&gt;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 } } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

4.2. 封装DeviceUtils

/**
 * [@author](/user/author) Tanranran
 * [@date](/user/date) 2024/5/14 22:20
 * [@description](/user/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 } } <button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

4.3. 使用

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

2、

import { DeviceUtils } from './DeviceUtils';
console.log(await DeviceUtils.getDeviceId())
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

5. 远程依赖

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

通过 ohpm 安装utilcode库。

ohpm i [@ranran](/user/ranran)/utilcode
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>

使用

import { DeviceUtils } from '[@ranran](/user/ranran)/utilcode';
console.log(await DeviceUtils.getDeviceId())
<button style="position: absolute; padding: 4px 8px 0px; cursor: pointer; top: 4px; right: 8px; font-size: 14px;">复制</button>
</markdown>

关于HarmonyOS 鸿蒙Next获取卸载APP后不变的设备ID的问题,您也可以访问:https://www.itying.com/category-93-b0.html 联系官网客服。
22 回复
升级系统,这个Id是否会变?
注意,使用AssetStore 需要声明  
ohos.permission.STORE_PERSISTENT_DATA 权限

升级HarmonyOS后,感觉手机的整体性能都有了很大的提升。

权限拒绝后还能用吗

这个权限,不需要申请。只需要声明即可

harmonyOS api9 不能用吗?提示 hvigor 不兼容

api 12 已经开放了。别再学api9 了。。。。

刚熟悉api9 现在api12好多不兼容api9,设备也不支持,要升级才可以,难

可以申请模拟器

资产数据是存入在系统层级的,系统是如何确保存入的数据可以不能被其他应用访问?

每个应用有自己的资产存储分区,别的应用无法访问

DeviceUtils.getDeviceId()返回的值还是undefined

是的,我这边也是undefined

将getDeviceId中的第二个判断if(!deviceId)修改为if (deviceId === ‘undefined’) 就可以正确获取到了。定位到原因是deviceId = ${(await AssetStore.get(DeviceUtils.deviceIdCacheKey)).data};的时候,如果get到的是undeifined,deviceId是字符串’undefined’而不是undefined本身

DeviceUtils.getDeviceId()中这里要判断一下undefined: deviceId = ${(await AssetStore.get(AssetStoreUtils.deviceIdCacheKey)).data ?? ''}

deviceId = util.generateRandomUUID(true).replace('-', '');

需要修改为:

deviceId = util.generateRandomUUID(true).replace(/-/g, '');

才可以实现全局替换哟

很重要的分享,赞一个。。。
AssetStore.updateAssetMap() 方法中调用asset.update传的参数有问题,会报错。

AssetStore.has()方法  return { isSuccess: true }; 漏了

oaid  系统升级的话,会变吗?

用户不重置,不会

回到顶部