鴻蒙獲取設(shè)備唯一ID

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
  }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市馁启,隨后出現(xiàn)的幾起案子驾孔,更是在濱河造成了極大的恐慌,老刑警劉巖惯疙,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翠勉,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡霉颠,警方通過(guò)查閱死者的電腦和手機(jī)对碌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蒿偎,“玉大人朽们,你說(shuō)我怎么就攤上這事∷呶唬” “怎么了骑脱?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)苍糠。 經(jīng)常有香客問(wèn)我叁丧,道長(zhǎng),這世上最難降的妖魔是什么岳瞭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任拥娄,我火速辦了婚禮,結(jié)果婚禮上瞳筏,老公的妹妹穿的比我還像新娘条舔。我一直安慰自己,他們只是感情好乏矾,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布孟抗。 她就那樣靜靜地躺著,像睡著了一般钻心。 火紅的嫁衣襯著肌膚如雪凄硼。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天捷沸,我揣著相機(jī)與錄音摊沉,去河邊找鬼。 笑死痒给,一個(gè)胖子當(dāng)著我的面吹牛说墨,可吹牛的內(nèi)容都是我干的骏全。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼尼斧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼姜贡!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起棺棵,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤楼咳,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后烛恤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體母怜,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年缚柏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了苹熏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡币喧,死狀恐怖轨域,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粱锐,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布扛邑,位于F島的核電站怜浅,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蔬崩。R本人自食惡果不足惜恶座,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沥阳。 院中可真熱鬧跨琳,春花似錦、人聲如沸桐罕。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)功炮。三九已至溅潜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間薪伏,已是汗流浹背滚澜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嫁怀,地道東北人设捐。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓借浊,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親萝招。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蚂斤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容