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。但是天无绝人之路,有一种取巧的办法可以实现。下面是具体办法。
Demo地址
https://github.com/tanranran/HarmonyUtilCode
3. 解决办法
在HarmonyOS NEXT中,有一个 ****@ohos.security.asset (关键资产存储服务) ****的API【类似于iOS中的Keychain services】,有一个特殊属性 IS_PERSISTENT,该特性可实现,在应用卸载时保留关键资产,利用该特性,我们可以随机生成一个32位的uuid,存储到ohos.security.asset中。
注意,使用AssetStore的IS_PERSISTENT特性 需要在module.json5中声明权限

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
}
}
- 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.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
4.2. 封装DeviceUtils
4.3. 使用
1、module.json5 中requestPermissions里增加ohos.permission.STORE_PERSISTENT_DATA 权限【只需要声明即可,不需要动态申请】
2、
5. 远程依赖
如果觉得上述源码方式集成到项目中比较麻烦,可以使用远程依赖的方式引入
通过 ohpm 安装utilcode库。
使用
本文正在参加华为鸿蒙有奖征文征文活动