1. 背景
在HarmonyOS NEXT中热幔,想要獲取設(shè)備ID砾隅,有3種方式
UDID: deviceinfo.udid 佑力,僅限系統(tǒng)應(yīng)用使用
AAID: aaid.getAAID()焕檬,然而卸載APP/恢復(fù)設(shè)備出廠設(shè)置/后會(huì)發(fā)生變化
OAID:identifier.getOAID盛险,同一臺(tái)設(shè)備上不同的App獲取到的OAID值一樣拌倍,但是用戶如果關(guān)閉跟蹤開(kāi)關(guān),該應(yīng)用僅能獲取到全0的OAID涯穷。且使用該API棍掐,需要申請(qǐng)申請(qǐng)廣告跟蹤權(quán)限ohos.permission.APP_TRACKING_CONSENT,觸發(fā)動(dòng)態(tài)授權(quán)彈框拷况,向用戶請(qǐng)求授權(quán)作煌,用戶授權(quán)成功后才可獲取。
2. 問(wèn)題
從上述三種方法中我們發(fā)現(xiàn)赚瘦,無(wú)法實(shí)現(xiàn) 不需要申請(qǐng)動(dòng)態(tài)權(quán)限粟誓,且App卸載后不變的設(shè)備ID。但是天無(wú)絕人之路起意,有一種取巧的辦法可以實(shí)現(xiàn)努酸。下面是具體辦法。
3. 源碼實(shí)現(xiàn)
3.1. 封裝AssetStore
import { asset } from '@kit.AssetStoreKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
/// AssetStore 操作結(jié)果
export interface AssetStoreResult {
isSuccess: boolean;
error?: BusinessError;
data?: string;
}
/// AssetStore query 操作結(jié)果
export interface AssetStoreQueryResult {
res?: asset.AssetMap[];
error?: BusinessError;
}
/**
* 基于 @ohos.security.asset 的封裝杜恰』裾可以保證『重裝/刪除應(yīng)用而不丟失數(shù)據(jù)』。
* @author Tanranran
* @date 2024/5/14 22:14
* @description
* 關(guān)鍵資產(chǎn)存儲(chǔ)服務(wù)提供了用戶短敏感數(shù)據(jù)的安全存儲(chǔ)及管理能力心褐。
* 其中舔涎,短敏感數(shù)據(jù)可以是密碼類(賬號(hào)/密碼)、Token類(應(yīng)用憑據(jù))逗爹、其他關(guān)鍵明文(如銀行卡號(hào))等長(zhǎng)度較短的用戶敏感數(shù)據(jù)亡嫌。
* 可在應(yīng)用卸載時(shí)保留數(shù)據(jù)。需要權(quán)限: 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 {
/**
* 新增數(shù)據(jù)
* 添加成功知染,會(huì)通過(guò) AppStorage 傳值值變更,外部可通過(guò) @StorageProp(key) value: string 觀察值變化斑胜。
* @param key 要添加的索引
* @param value 要添加的值
* @param isPersistent 在應(yīng)用卸載時(shí)是否需要保留關(guān)鍵資產(chǎn)控淡,默認(rèn)為 true
* @returns Promise<AssetStoreResult> 表示添加操作的異步結(jié)果
*/
public static async set(key: string, value: string, isPersistent: boolean = true): Promise<AssetStoreResult> {
let attr: asset.AssetMap = new Map();
if (canIUse("SystemCapability.Security.Asset")) {
// 關(guān)鍵資產(chǎn)別名嫌吠,每條關(guān)鍵資產(chǎn)的唯一索引。
// 類型為Uint8Array掺炭,長(zhǎng)度為1-256字節(jié)辫诅。
attr.set(asset.Tag.ALIAS, AssetStore.stringToArray(key));
// 關(guān)鍵資產(chǎn)明文。
// 類型為Uint8Array涧狮,長(zhǎng)度為1-1024字節(jié)
attr.set(asset.Tag.SECRET, AssetStore.stringToArray(value));
// 關(guān)鍵資產(chǎn)同步類型>THIS_DEVICE只在本設(shè)備進(jìn)行同步炕矮,如僅在本設(shè)備還原的備份場(chǎng)景。
attr.set(asset.Tag.SYNC_TYPE, asset.SyncType.THIS_DEVICE);
//枚舉者冤,新增關(guān)鍵資產(chǎn)時(shí)的沖突(如:別名相同)處理策略吧享。OVERWRITE》拋出異常,由業(yè)務(wù)進(jìn)行后續(xù)處理譬嚣。
attr.set(asset.Tag.CONFLICT_RESOLUTION,asset.ConflictResolution.THROW_ERROR)
// 在應(yīng)用卸載時(shí)是否需要保留關(guān)鍵資產(chǎn)。
// 需要權(quán)限: 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}`);
// 添加成功,會(huì)通過(guò) AppStorage 傳值值變更遭垛,外部可通過(guò) @StorageProp(key) value: string 觀察值變化尼桶。
AppStorage.setOrCreate(key, value);
}
return result;
}
/**
* 新增數(shù)據(jù)
* @param attr 要添加的屬性集
* @returns Promise<AssetStoreResult> 表示添加操作的異步結(jié)果
*/
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 };
}
}
/**
* 刪除數(shù)據(jù)
* 刪除成功,會(huì)通過(guò) AppStorage 傳值值變更锯仪,外部可通過(guò) @StorageProp(key) value: string 觀察值變化泵督。
* AppStorage API12 及以上支持 undefined 和 null類型。
* @param key 要?jiǎng)h除的索引
* @returns Promise<AssetStoreResult> 表示添加操作的異步結(jié)果
*/
public static async remove(key: string) {
let query: asset.AssetMap = new Map();
if (canIUse("SystemCapability.Security.Asset")) {
// 關(guān)鍵資產(chǎn)別名庶喜,每條關(guān)鍵資產(chǎn)的唯一索引小腊。
// 類型為Uint8Array,長(zhǎng)度為1-256字節(jié)久窟。
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}`);
// 刪除成功秩冈,會(huì)通過(guò) AppStorage 傳值值變更,外部可通過(guò) @StorageProp(key) value: string 觀察值變化斥扛。
// AppStorage API12 及以上支持 undefined 和 null類型入问。
AppStorage.setOrCreate(key, '');
}
return result;
}
/**
* 刪除數(shù)據(jù)
* @param attr 要?jiǎng)h除的屬性集
* @returns Promise<AssetStoreResult> 表示添加操作的異步結(jié)果
*/
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 };
}
}
/**
* 判斷是否存在 數(shù)據(jù)
* @param key 要查找的索引
* @returns Promise<AssetStoreResult> 表示添加操作的異步結(jié)果
*/
public static async has(key: string): Promise<AssetStoreResult> {
if (canIUse("SystemCapability.Security.Asset")) {
let query: asset.AssetMap = new Map();
// 關(guān)鍵資產(chǎn)別名,每條關(guān)鍵資產(chǎn)的唯一索引稀颁。
// 類型為Uint8Array芬失,長(zhǎng)度為1-256字節(jié)。
query.set(asset.Tag.ALIAS, AssetStore.stringToArray(key));
// 關(guān)鍵資產(chǎn)查詢返回的結(jié)果類型匾灶。
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 };
}
/**
* 查找數(shù)據(jù)
* @param key 要查找的索引
* @returns Promise<AssetStoreResult> 表示添加操作的異步結(jié)果
*/
public static async get(key: string): Promise<AssetStoreResult> {
if (canIUse("SystemCapability.Security.Asset")) {
let query: asset.AssetMap = new Map();
// 關(guān)鍵資產(chǎn)別名棱烂,每條關(guān)鍵資產(chǎn)的唯一索引。
// 類型為Uint8Array阶女,長(zhǎng)度為1-256字節(jié)垢啼。
query.set(asset.Tag.ALIAS, AssetStore.stringToArray(key));
// 關(guān)鍵資產(chǎn)查詢返回的結(jié)果類型窜锯。
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: "" };
}
/**
* 查找數(shù)據(jù)
* @param key 要查找的索引
* @returns Promise<AssetStoreQueryResult> 表示添加操作的異步結(jié)果
*/
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 };
}
}
/**
* 更新數(shù)據(jù)
* @param query 要更新的索引數(shù)據(jù)集
* @param attrsToUpdate 要更新的數(shù)據(jù)集
* @returns Promise<AssetStoreResult> 表示添加操作的異步結(jié)果
*/
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: "不支持該平臺(tái)" } as BusinessError
}
}
3.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 = ""
/**
* 獲取設(shè)備id>32為隨機(jī)碼[卸載APP后依舊不變]
* @param isMD5
* @returns
*/
static async getDeviceId() {
let deviceId = DeviceUtils.deviceId
//如果內(nèi)存緩存為空,則從AssetStore中讀取
if (!deviceId) {
deviceId = `${(await AssetStore.get(DeviceUtils.deviceIdCacheKey)).data}`
}
//如果AssetStore中未讀取到芭析,則隨機(jī)生成32位隨機(jī)碼锚扎,然后緩存到AssetStore中
if (!deviceId) {
deviceId = util.generateRandomUUID(true).replace('-', '')
AssetStore.set(DeviceUtils.deviceIdCacheKey, deviceId)
}
DeviceUtils.deviceId = deviceId
return deviceId
}
}