【HarmonyOS NEXT】獲取解除安裝APP後不變的裝置ID

tanranran發表於2024-05-15

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())

本文正在參加華為鴻蒙有獎徵文徵文活動

相關文章