HarmonyOS开发从隐私看板到数据删除,构建用户信任的隐私防线

举报
Jack20 发表于 2026/06/20 19:25:41 2026/06/20
【摘要】 HarmonyOS APP开发中的隐私设置全解析:从隐私看板到数据删除,构建用户信任的隐私防线📌 核心要点:HarmonyOS 提供完整的隐私管理框架,涵盖隐私看板、权限使用记录、广告ID管理、数据删除请求和隐私合规开发,帮助开发者构建可信赖的应用 一、背景与动机2024 年,某知名 App 因"偷偷调用摄像头"被曝光,下载量一夜之间暴跌 40%。这不是个例——用户对隐私的敏感度前所未有...

HarmonyOS APP开发中的隐私设置全解析:从隐私看板到数据删除,构建用户信任的隐私防线

📌 核心要点:HarmonyOS 提供完整的隐私管理框架,涵盖隐私看板、权限使用记录、广告ID管理、数据删除请求和隐私合规开发,帮助开发者构建可信赖的应用


一、背景与动机

2024 年,某知名 App 因"偷偷调用摄像头"被曝光,下载量一夜之间暴跌 40%。这不是个例——用户对隐私的敏感度前所未有地高,一条"该应用正在使用你的位置"的系统提示,就可能让用户直接卸载。

隐私不是"锦上添花",而是"生死线"。在 HarmonyOS 生态中,隐私合规更是上架审核的硬性要求。如果你的应用没有正确处理权限申请、没有提供数据删除入口、没有尊重用户的隐私选择——对不起,审核都过不了。

HarmonyOS 提供了一套完整的隐私管理框架,从隐私看板(让用户看到谁在用什么权限)到数据删除请求(让用户可以"被遗忘"),覆盖了隐私保护的完整生命周期。

典型使用场景:

  • 隐私看板:展示应用权限使用情况,增强透明度
  • 权限使用记录:记录每次权限调用的详细日志
  • 广告ID管理:尊重用户的广告追踪选择
  • 数据删除请求:响应用户的"被遗忘权"
  • 隐私合规开发:遵循最小权限原则和透明度原则

二、核心原理

2.1 隐私管理体系架构

flowchart TB
    A[用户] --> B[隐私看板]
    A --> C[权限管理]
    A --> D[广告ID设置]
    A --> E[数据删除请求]
    
    B --> B1[权限使用概览]
    B --> B2[敏感行为记录]
    B --> B3[风险提醒]
    
    C --> C1[权限申请]
    C --> C2[权限授权]
    C --> C3[权限撤销]
    C --> C4[使用记录]
    
    D --> D1[广告ID生成]
    D --> D2[重置广告ID]
    D --> D3[限制广告追踪]
    
    E --> E1[请求数据删除]
    E --> E2[应用处理删除]
    E --> E3[确认删除完成]
    
    F[开发者] --> G[隐私合规开发]
    G --> G1[最小权限原则]
    G --> G2[透明度原则]
    G --> G3[数据最小化]
    G --> G4[安全存储]
    
    classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
    classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
    classDef error fill:#F44336,stroke:#D32F2F,color:#fff
    classDef info fill:#2196F3,stroke:#1976D2,color:#fff
    classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
    
    class A primary
    class B,C,D,E info
    class B1,B2,B3,C1,C2,C3,C4,D1,D2,D3,E1,E2,E3 purple
    class F warning
    class G error
    class G1,G2,G3,G4 primary

2.2 权限使用记录机制

HarmonyOS 会自动记录应用使用敏感权限的行为,包括:

记录项 说明
权限名称 使用的具体权限(如 CAMERA、LOCATION)
使用时间 精确到秒的时间戳
使用时长 持续使用的时间
访问频率 一段时间内的使用次数
前后台状态 使用时应用是否在前台

2.3 隐私合规开发流程

flowchart LR
    A[需求分析] --> B{是否需要敏感权限?}
    B -->|| C[正常开发]
    B -->|| D[评估必要性]
    D --> E{是否必须?}
    E -->|| F[寻找替代方案]
    E -->|| G[运行时申请权限]
    G --> H[用户授权?]
    H -->|| I[使用权限+记录日志]
    H -->|| J[降级处理]
    I --> K[用完即释放]
    
    classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
    classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
    classDef error fill:#F44336,stroke:#D32F2F,color:#fff
    classDef info fill:#2196F3,stroke:#1976D2,color:#fff
    classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
    
    class A primary
    class B,D,E,H info
    class C,G,I,K primary
    class F,J warning

核心理念:最小权限原则——能用不敏感的权限就别用敏感的,能不用权限就别申请。就像你不会把家里所有房间的钥匙都交给保洁阿姨,只给她需要打扫的房间钥匙就够了。


三、代码实战

3.1 隐私看板与权限使用记录:让权限使用透明化

隐私看板是 HarmonyOS 的系统级功能,应用开发者需要确保权限使用行为被正确记录和展示。

import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 权限使用记录项
 */
interface PermissionUsageRecord {
  permission: string;       // 权限名称
  permissionName: string;   // 权限中文名
  lastUsedTime: number;     // 最后使用时间
  useCount: number;         // 使用次数
  isForeground: boolean;    // 是否前台使用
}

/**
 * 隐私看板管理器
 * 管理应用的权限使用记录和隐私信息展示
 */
class PrivacyBoardManager {
  private atManager: abilityAccessCtrl.AtManager | null = null;
  private usageRecords: Map<string, PermissionUsageRecord> = new Map();

  /**
   * 初始化
   */
  init(): void {
    this.atManager = abilityAccessCtrl.createAtManager();
    console.info('[Privacy] 隐私看板管理器初始化成功');
  }

  /**
   * 记录权限使用
   * 在每次使用敏感权限时调用,确保隐私看板数据准确
   * @param permission 权限名称
   * @param isForeground 是否前台使用
   */
  recordPermissionUsage(permission: string, isForeground: boolean = true): void {
    const existing = this.usageRecords.get(permission);
    const now = Date.now();

    if (existing) {
      existing.lastUsedTime = now;
      existing.useCount += 1;
      existing.isForeground = isForeground;
    } else {
      this.usageRecords.set(permission, {
        permission: permission,
        permissionName: this.getPermissionDisplayName(permission),
        lastUsedTime: now,
        useCount: 1,
        isForeground: isForeground,
      });
    }

    console.info(`[Privacy] 记录权限使用: ${permission}, 前台=${isForeground}`);
  }

  /**
   * 获取所有权限使用记录
   */
  getUsageRecords(): PermissionUsageRecord[] {
    return Array.from(this.usageRecords.values())
      .sort((a, b) => b.lastUsedTime - a.lastUsedTime); // 按时间倒序
  }

  /**
   * 获取高风险权限使用统计
   * 敏感权限:相机、麦克风、位置、通讯录
   */
  getSensitiveUsageSummary(): Record<string, number> {
    const sensitivePermissions = [
      'ohos.permission.CAMERA',
      'ohos.permission.MICROPHONE',
      'ohos.permission.LOCATION',
      'ohos.permission.APPROXIMATELY_LOCATION',
      'ohos.permission.READ_CONTACTS',
    ];

    const summary: Record<string, number> = {};
    for (const perm of sensitivePermissions) {
      const record = this.usageRecords.get(perm);
      summary[this.getPermissionDisplayName(perm)] = record ? record.useCount : 0;
    }
    return summary;
  }

  /**
   * 检查权限是否已授权
   */
  async checkPermission(tokenID: number, permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
    if (!this.atManager) return abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
    try {
      const result = await this.atManager.checkAccessToken(tokenID, permission);
      return result;
    } catch (err) {
      console.error(`[Privacy] 检查权限失败: ${JSON.stringify(err)}`);
      return abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
    }
  }

  /**
   * 请求权限(带隐私说明)
   * @param context 上下文
   * @param permissions 权限列表
   * @param reason 申请原因(展示给用户)
   */
  async requestPermissionsWithReason(
    context: Context,
    permissions: Array<Permissions>,
    reason: string
  ): Promise<Record<string, boolean>> {
    if (!this.atManager) return {};

    const results: Record<string, boolean> = {};

    try {
      // 逐个请求权限,每个都附带原因说明
      for (const permission of permissions) {
        console.info(`[Privacy] 请求权限: ${permission}, 原因: ${reason}`);

        const result = await this.atManager.requestPermissionsFromUser(context, [permission]);
        const granted = result.authResults[0] === 0;
        results[permission] = granted;

        if (granted) {
          this.recordPermissionUsage(permission, true);
        }
      }
    } catch (err) {
      console.error(`[Privacy] 请求权限失败: ${JSON.stringify(err)}`);
    }

    return results;
  }

  /**
   * 获取权限显示名称
   */
  private getPermissionDisplayName(permission: string): string {
    const nameMap: Record<string, string> = {
      'ohos.permission.CAMERA': '📷 相机',
      'ohos.permission.MICROPHONE': '🎤 麦克风',
      'ohos.permission.LOCATION': '📍 精确位置',
      'ohos.permission.APPROXIMATELY_LOCATION': '📍 大致位置',
      'ohos.permission.READ_CONTACTS': '👥 读取通讯录',
      'ohos.permission.WRITE_CONTACTS': '👥 写入通讯录',
      'ohos.permission.READ_CALENDAR': '📅 读取日历',
      'ohos.permission.READ_MEDIA': '🖼️ 读取媒体文件',
      'ohos.permission.WRITE_MEDIA': '🖼️ 写入媒体文件',
      'ohos.permission.ACTIVITY_MOTION': '🏃 运动数据',
      'ohos.permission.READ_HEALTH_DATA': '❤️ 健康数据',
    };
    return nameMap[permission] || permission;
  }
}

/**
 * 隐私看板页面
 */
@Entry
@Component
struct PrivacyBoardPage {
  @State usageRecords: PermissionUsageRecord[] = [];
  @State sensitiveSummary: Record<string, number> = {};
  private boardManager: PrivacyBoardManager = new PrivacyBoardManager();

  aboutToAppear(): void {
    this.boardManager.init();

    // 模拟记录一些权限使用
    this.boardManager.recordPermissionUsage('ohos.permission.CAMERA', true);
    this.boardManager.recordPermissionUsage('ohos.permission.LOCATION', true);
    this.boardManager.recordPermissionUsage('ohos.permission.LOCATION', false);
    this.boardManager.recordPermissionUsage('ohos.permission.MICROPHONE', true);

    // 获取记录
    this.usageRecords = this.boardManager.getUsageRecords();
    this.sensitiveSummary = this.boardManager.getSensitiveUsageSummary();
  }

  build() {
    Scroll() {
      Column() {
        Text('🛡️ 隐私看板')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 8 })

        Text('查看本应用的权限使用情况')
          .fontSize(14)
          .fontColor('#666')
          .margin({ bottom: 24 })

        // 敏感权限使用概览
        Column() {
          Text('敏感权限使用统计')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })

          ForEach(Object.entries(this.sensitiveSummary), ([name, count]: [string, number]) => {
            Row() {
              Text(name)
                .fontSize(14)
                .layoutWeight(1)
              Text(`${count}`)
                .fontSize(14)
                .fontColor(count > 0 ? '#FF9800' : '#999')
                .fontWeight(count > 0 ? FontWeight.Bold : FontWeight.Normal)
            }
            .width('100%')
            .padding({ top: 8, bottom: 8 })
            .borderRadius(8)
          })
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#FFF3E0')
        .borderRadius(12)
        .margin({ bottom: 20 })

        // 详细使用记录
        Column() {
          Text('权限使用记录')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })

          ForEach(this.usageRecords, (record: PermissionUsageRecord) => {
            Row() {
              Column() {
                Text(record.permissionName)
                  .fontSize(14)
                  .fontWeight(FontWeight.Medium)
                Text(this.formatTime(record.lastUsedTime))
                  .fontSize(12)
                  .fontColor('#999')
                  .margin({ top: 4 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              Column() {
                Text(`${record.useCount}`)
                  .fontSize(14)
                  .fontColor('#4CAF50')
                Text(record.isForeground ? '前台' : '后台')
                  .fontSize(11)
                  .fontColor(record.isForeground ? '#4CAF50' : '#FF9800')
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.End)
            }
            .width('100%')
            .padding(12)
            .backgroundColor('#fff')
            .borderRadius(8)
            .margin({ bottom: 8 })
          })
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#f5f5f5')
        .borderRadius(12)
      }
      .padding(20)
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 格式化时间
   */
  private formatTime(timestamp: number): string {
    const date = new Date(timestamp);
    const hours = date.getHours().toString().padStart(2, '0');
    const minutes = date.getMinutes().toString().padStart(2, '0');
    return `${date.getMonth() + 1}/${date.getDate()} ${hours}:${minutes}`;
  }
}

export { PrivacyBoardManager, PermissionUsageRecord };

3.2 广告ID管理与数据删除请求:尊重用户的选择权

广告ID和"被遗忘权"是隐私合规的两个关键要求。用户应该能控制广告追踪,也应该能删除自己的数据。

import { identifier } from '@kit.ArkData';
import { preferences } from '@kit.ArkData';

/**
 * 广告ID管理器
 * 管理广告标识符的获取、重置和限制追踪
 */
class AdvertisingIdManager {
  private prefs: preferences.Preferences | null = null;

  /**
   * 初始化
   */
  async init(context: Context): Promise<void> {
    this.prefs = await preferences.getPreferences(context, 'ad_privacy');
  }

  /**
   * 获取广告ID
   * 注意:必须尊重用户的"限制广告追踪"选择
   */
  async getAdvertisingId(): Promise<string> {
    try {
      // 先检查用户是否限制了广告追踪
      const isLimitAdTracking = await this.isLimitAdTracking();
      if (isLimitAdTracking) {
        console.info('[AdId] 用户已限制广告追踪,不返回广告ID');
        return ''; // 返回空字符串,不返回任何标识符
      }

      // 获取广告ID
      const adId = await identifier.getAdvertisingId();
      console.info(`[AdId] 获取广告ID: ${adId ? '成功' : '为空'}`);
      return adId || '';
    } catch (err) {
      console.error(`[AdId] 获取广告ID失败: ${JSON.stringify(err)}`);
      return '';
    }
  }

  /**
   * 检查用户是否限制了广告追踪
   */
  async isLimitAdTracking(): Promise<boolean> {
    try {
      const isLimited = await identifier.isLimitAdTrackingEnabled();
      console.info(`[AdId] 限制广告追踪: ${isLimited ? '是' : '否'}`);
      return isLimited;
    } catch (err) {
      console.error(`[AdId] 检查限制追踪失败: ${JSON.stringify(err)}`);
      // 出错时默认尊重隐私,返回 true
      return true;
    }
  }

  /**
   * 获取广告ID使用记录
   * 用于隐私看板展示
   */
  async getAdIdUsageLog(): Promise<Record<string, string>> {
    if (!this.prefs) return {};

    const lastAccess = this.prefs.getStringSync('ad_id_last_access', '从未');
    const accessCount = this.prefs.getNumberSync('ad_id_access_count', 0);

    return {
      '最后访问时间': lastAccess,
      '访问次数': `${accessCount}`,
    };
  }

  /**
   * 记录广告ID访问(用于透明度)
   */
  private async logAdIdAccess(): Promise<void> {
    if (!this.prefs) return;

    const now = new Date().toLocaleString('zh-CN');
    await this.prefs.put('ad_id_last_access', now);

    const count = this.prefs.getNumberSync('ad_id_access_count', 0);
    await this.prefs.put('ad_id_access_count', count + 1);

    await this.prefs.flush();
  }
}

/**
 * 数据删除请求管理器
 * 响应用户的"被遗忘权"请求
 */
class DataDeletionManager {
  private prefs: preferences.Preferences | null = null;

  /**
   * 初始化
   */
  async init(context: Context): Promise<void> {
    this.prefs = await preferences.getPreferences(context, 'data_deletion');
  }

  /**
   * 提交数据删除请求
   * @param dataType 要删除的数据类型
   * @param reason 删除原因
   */
  async submitDeletionRequest(
    dataType: 'all' | 'profile' | 'history' | 'cache',
    reason: string
  ): Promise<{ requestId: string; estimatedDays: number }> {
    // 生成请求ID
    const requestId = `DEL_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;

    // 估算处理时间
    const estimatedDays = dataType === 'all' ? 7 : 3;

    // 保存请求记录
    if (this.prefs) {
      await this.prefs.put(`request_${requestId}`, JSON.stringify({
        requestId,
        dataType,
        reason,
        submitTime: Date.now(),
        status: 'pending',
      }));
      await this.prefs.flush();
    }

    console.info(`[Deletion] 数据删除请求已提交: ${requestId}, 类型=${dataType}`);

    // 在实际项目中,这里应该调用后端 API 提交删除请求
    // 此处模拟本地处理
    await this.processDeletion(dataType);

    return { requestId, estimatedDays };
  }

  /**
   * 处理数据删除
   * 根据数据类型执行对应的删除操作
   */
  private async processDeletion(dataType: string): Promise<void> {
    switch (dataType) {
      case 'all':
        // 删除所有用户数据
        await this.deleteAllUserData();
        break;
      case 'profile':
        // 仅删除个人资料
        await this.deleteProfileData();
        break;
      case 'history':
        // 仅删除浏览/使用历史
        await this.deleteHistoryData();
        break;
      case 'cache':
        // 仅删除缓存
        await this.deleteCacheData();
        break;
    }
  }

  /**
   * 删除所有用户数据
   */
  private async deleteAllUserData(): Promise<void> {
    console.info('[Deletion] 开始删除所有用户数据');

    // 1. 清除偏好设置
    if (this.prefs) {
      await this.prefs.clear();
      await this.prefs.flush();
    }

    // 2. 清除本地数据库
    // 在实际项目中,删除 relationalStore 数据库文件

    // 3. 清除文件缓存
    // 在实际项目中,删除应用沙箱内的缓存文件

    // 4. 通知服务端删除云端数据
    // 在实际项目中,调用后端 API 删除服务端数据

    console.info('[Deletion] 所有用户数据删除完成');
  }

  /**
   * 删除个人资料数据
   */
  private async deleteProfileData(): Promise<void> {
    console.info('[Deletion] 删除个人资料数据');
    // 删除昵称、头像、生日等个人资料
  }

  /**
   * 删除历史数据
   */
  private async deleteHistoryData(): Promise<void> {
    console.info('[Deletion] 删除浏览/使用历史');
    // 删除搜索历史、浏览记录、使用记录
  }

  /**
   * 删除缓存数据
   */
  private async deleteCacheData(): Promise<void> {
    console.info('[Deletion] 删除缓存数据');
    // 删除图片缓存、API 缓存、临时文件
  }

  /**
   * 获取删除请求状态
   */
  async getDeletionStatus(requestId: string): Promise<string> {
    if (!this.prefs) return 'unknown';
    const data = this.prefs.getStringSync(`request_${requestId}`, '');
    if (!data) return 'not_found';

    try {
      const record = JSON.parse(data);
      return record.status;
    } catch {
      return 'error';
    }
  }
}

/**
 * 隐私管理页面
 */
@Entry
@Component
struct PrivacyManagementPage {
  @State isLimitAdTracking: boolean = false;
  @State adIdStatus: string = '未获取';
  @State deletionStatus: string = '';
  @State showDeletionConfirm: boolean = false;

  private adIdManager: AdvertisingIdManager = new AdvertisingIdManager();
  private deletionManager: DataDeletionManager = new DataDeletionManager();

  async aboutToAppear(): Promise<void> {
    const context = getContext(this);
    await this.adIdManager.init(context);
    await this.deletionManager.init(context);

    // 检查广告追踪状态
    this.isLimitAdTracking = await this.adIdManager.isLimitAdTracking();
    this.adIdStatus = this.isLimitAdTracking ? '已限制追踪' : '允许追踪';
  }

  build() {
    Scroll() {
      Column() {
        Text('🔒 隐私管理')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 24 })

        // 广告ID管理
        Column() {
          Text('📢 广告追踪设置')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })

          Row() {
            Text('限制广告追踪')
              .fontSize(14)
              .layoutWeight(1)
            Toggle({ type: ToggleType.Switch, isOn: this.isLimitAdTracking })
              .onChange(async (isOn: boolean) => {
                // 注意:限制广告追踪是系统级设置
                // 应用只能读取状态,不能直接修改
                // 这里引导用户去系统设置
                this.isLimitAdTracking = isOn;
                if (isOn) {
                  this.adIdStatus = '已限制追踪';
                } else {
                  this.adIdStatus = '允许追踪';
                }
              })
          }
          .width('100%')

          Row() {
            Text('当前状态:')
              .fontSize(12)
              .fontColor('#666')
            Text(this.adIdStatus)
              .fontSize(12)
              .fontColor(this.isLimitAdTracking ? '#4CAF50' : '#FF9800')
              .fontWeight(FontWeight.Bold)
          }
          .width('100%')
          .margin({ top: 8 })

          if (this.isLimitAdTracking) {
            Text('✅ 您已选择限制广告追踪,应用不会使用广告ID进行个性化推荐')
              .fontSize(12)
              .fontColor('#4CAF50')
              .margin({ top: 8 })
          } else {
            Text('⚠️ 广告追踪未限制,应用可能使用广告ID进行个性化推荐')
              .fontSize(12)
              .fontColor('#FF9800')
              .margin({ top: 8 })
          }
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#E8F5E9')
        .borderRadius(12)
        .margin({ bottom: 20 })

        // 数据删除请求
        Column() {
          Text('🗑️ 数据管理')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 12 })

          Text('根据相关法律法规,您有权要求删除您的个人数据。')
            .fontSize(12)
            .fontColor('#666')
            .margin({ bottom: 16 })

          // 删除选项
          Column() {
            this.DeletionOption('仅删除缓存数据', 'cache', '清除本地缓存,不影响账号数据')
            this.DeletionOption('删除浏览历史', 'history', '清除搜索和浏览记录')
            this.DeletionOption('删除个人资料', 'profile', '清除昵称、头像等个人资料')
            this.DeletionOption('删除所有数据', 'all', '⚠️ 此操作不可恢复,将删除所有数据')
          }
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#FFF3E0')
        .borderRadius(12)
        .margin({ bottom: 20 })

        // 删除确认弹窗
        if (this.showDeletionConfirm) {
          Column() {
            Text('⚠️ 确认删除')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
              .margin({ bottom: 12 })

            Text(this.deletionStatus)
              .fontSize(14)
              .fontColor('#F44336')
              .margin({ bottom: 16 })

            Row() {
              Button('取消')
                .backgroundColor('#e0e0e0')
                .fontColor('#333')
                .onClick(() => {
                  this.showDeletionConfirm = false;
                })
              Button('确认删除')
                .backgroundColor('#F44336')
                .onClick(async () => {
                  const result = await this.deletionManager.submitDeletionRequest('all', '用户主动请求');
                  this.deletionStatus = `请求已提交,预计 ${result.estimatedDays} 天内完成`;
                  this.showDeletionConfirm = false;
                })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceAround)
          }
          .width('80%')
          .padding(20)
          .backgroundColor('#fff')
          .borderRadius(16)
          .shadow({ radius: 20, color: '#40000000' })
        }
      }
      .padding(20)
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  DeletionOption(title: string, type: string, desc: string) {
    Row() {
      Column() {
        Text(title)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
        Text(desc)
          .fontSize(11)
          .fontColor('#999')
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Button('删除')
        .fontSize(12)
        .height(28)
        .backgroundColor(type === 'all' ? '#F44336' : '#FF9800')
        .onClick(async () => {
          if (type === 'all') {
            this.deletionStatus = '确定要删除所有数据吗?此操作不可恢复!';
            this.showDeletionConfirm = true;
          } else {
            await this.deletionManager.submitDeletionRequest(
              type as 'all' | 'profile' | 'history' | 'cache',
              '用户主动请求'
            );
          }
        })
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#fff')
    .borderRadius(8)
    .margin({ bottom: 8 })
  }
}

export { AdvertisingIdManager, DataDeletionManager };

3.3 隐私合规开发:最小权限与安全存储的最佳实践

隐私合规不是"加个开关"就完事的,它贯穿整个开发流程。以下是一个完整的隐私合规开发模板。

import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { cipherFramework } from '@kit.CryptoArchitectureKit';
import { preferences } from '@kit.ArkData';

/**
 * 隐私合规开发工具集
 * 提供权限管理、数据加密、隐私声明等能力
 */
class PrivacyComplianceKit {
  private atManager: abilityAccessCtrl.AtManager | null = null;

  /**
   * 权限分级定义
   * 根据敏感程度分级,指导权限申请策略
   */
  static readonly PERMISSION_LEVELS: Record<string, { level: string; risk: string; alternative: string }> = {
    'ohos.permission.CAMERA': {
      level: '高',
      risk: '可获取用户影像',
      alternative: '让用户从相册选择图片',
    },
    'ohos.permission.MICROPHONE': {
      level: '高',
      risk: '可获取用户声音',
      alternative: '让用户输入文字代替语音',
    },
    'ohos.permission.LOCATION': {
      level: '高',
      risk: '可获取用户精确位置',
      alternative: '使用大致位置或手动选择城市',
    },
    'ohos.permission.APPROXIMATELY_LOCATION': {
      level: '中',
      risk: '可获取用户大致位置',
      alternative: '让用户手动选择城市',
    },
    'ohos.permission.READ_CONTACTS': {
      level: '高',
      risk: '可读取用户通讯录',
      alternative: '让用户手动输入联系人信息',
    },
    'ohos.permission.READ_MEDIA': {
      level: '中',
      risk: '可读取用户媒体文件',
      alternative: '使用系统选择器让用户选择文件',
    },
  };

  init(): void {
    this.atManager = abilityAccessCtrl.createAtManager();
  }

  /**
   * 安全的权限请求流程
   * 1. 检查是否已有权限
   * 2. 检查是否应该展示申请理由
   * 3. 申请权限
   * 4. 处理授权结果
   */
  async requestPermissionSafely(
    context: Context,
    permission: Permissions,
    reason: string
  ): Promise<{ granted: boolean; shouldShowRationale: boolean }> {
    if (!this.atManager) {
      return { granted: false, shouldShowRationale: false };
    }

    try {
      // 第一步:检查是否已授权
      const tokenID = await this.getTokenId(context);
      const status = await this.atManager.checkAccessToken(tokenID, permission);

      if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
        console.info(`[Compliance] 权限已授予: ${permission}`);
        return { granted: true, shouldShowRationale: false };
      }

      // 第二步:申请权限
      console.info(`[Compliance] 请求权限: ${permission}, 原因: ${reason}`);
      const result = await this.atManager.requestPermissionsFromUser(context, [permission]);

      const granted = result.authResults[0] === 0;

      // 第三步:判断是否应该展示申请理由
      // 如果用户选择了"拒绝且不再询问",shouldShowRationale 为 false
      const shouldShowRationale = !granted && result.dialogShownResults?.[0] !== false;

      if (!granted) {
        // 用户拒绝,提供替代方案
        const permInfo = PrivacyComplianceKit.PERMISSION_LEVELS[permission];
        if (permInfo) {
          console.info(`[Compliance] 权限被拒绝,替代方案: ${permInfo.alternative}`);
        }
      }

      return { granted, shouldShowRationale };
    } catch (err) {
      console.error(`[Compliance] 权限请求异常: ${JSON.stringify(err)}`);
      return { granted: false, shouldShowRationale: false };
    }
  }

  /**
   * 获取应用 TokenID
   */
  private async getTokenId(context: Context): Promise<number> {
    const bundleInfo = await bundleManager.getBundleInfoForSelf(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
    );
    return bundleInfo.appInfo.accessTokenId;
  }

  /**
   * 敏感数据加密存储
   * 隐私数据不应明文存储
   */
  async encryptAndStore(
    context: Context,
    key: string,
    plainText: string,
    secretKey: string
  ): Promise<boolean> {
    try {
      // 使用 AES 加密
      const cipher = cipherFramework.createCipher('AES256|GCM|PKCS7');
      const symKeyGenerator = cipherFramework.createSymKeyGenerator('AES256');

      // 生成密钥
      const keyData = new Uint8Array(Buffer.from(secretKey.padEnd(32, '0'), 'utf-8'));
      const symKey = await symKeyGenerator.convertKey({ data: keyData });

      // 初始化加密
      await cipher.init(cipherFramework.CryptoMode.ENCRYPT_MODE, symKey, null);

      // 加密
      const input = { data: new Uint8Array(Buffer.from(plainText, 'utf-8')) };
      const encrypted = await cipher.doFinal(input);

      // 存储加密后的数据
      const prefs = await preferences.getPreferences(context, 'encrypted_data');
      await prefs.put(key, Buffer.from(encrypted.data).toString('base64'));
      await prefs.flush();

      console.info(`[Compliance] 敏感数据已加密存储: ${key}`);
      return true;
    } catch (err) {
      console.error(`[Compliance] 加密存储失败: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 生成隐私声明文本
   * 用于应用首次启动时的隐私弹窗
   */
  getPrivacyStatement(): string {
    return `隐私声明

本应用重视您的隐私保护,承诺:

1. 最小权限原则:仅申请实现功能所必需的权限
2. 透明度原则:每次使用敏感权限都会告知您
3. 数据最小化:仅收集实现功能所必需的数据
4. 安全存储:您的敏感数据均经过加密存储
5. 被遗忘权:您可以随时请求删除您的所有数据

本应用使用的敏感权限及用途:
• 📷 相机 - 用于扫描二维码
• 📍 位置 - 用于展示附近的服务点
• 📢 广告ID - 用于个性化推荐(您可以在设置中关闭)

您可以随时在"设置 > 隐私管理"中查看和管理您的隐私选项。`;
  }
}

/**
 * 隐私合规首次启动页面
 */
@Entry
@Component
struct PrivacyConsentPage {
  @State hasAgreed: boolean = false;
  @State showPrivacyDialog: boolean = true;
  private complianceKit: PrivacyComplianceKit = new PrivacyComplianceKit();

  async aboutToAppear(): Promise<void> {
    this.complianceKit.init();

    // 检查是否已同意隐私声明
    const context = getContext(this);
    const prefs = await preferences.getPreferences(context, 'privacy');
    this.hasAgreed = prefs.getBooleanSync('privacy_agreed', false);

    if (this.hasAgreed) {
      this.showPrivacyDialog = false;
    }
  }

  build() {
    Stack() {
      // 主内容
      Column() {
        Text('欢迎使用本应用')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)

      // 隐私声明弹窗
      if (this.showPrivacyDialog) {
        Column() {
          Text('📋 隐私声明')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 16 })

          Scroll() {
            Text(this.complianceKit.getPrivacyStatement())
              .fontSize(13)
              .lineHeight(22)
              .fontColor('#333')
          }
          .height(300)
          .width('100%')
          .margin({ bottom: 16 })

          Row() {
            Button('不同意')
              .backgroundColor('#e0e0e0')
              .fontColor('#333')
              .layoutWeight(1)
              .onClick(() => {
                // 用户不同意,退出应用
                console.warn('[Privacy] 用户不同意隐私声明,应用退出');
              })
            Button('同意并继续')
              .backgroundColor('#4CAF50')
              .fontColor('#fff')
              .layoutWeight(1)
              .onClick(async () => {
                this.hasAgreed = true;
                this.showPrivacyDialog = false;

                // 保存同意记录
                const context = getContext(this);
                const prefs = await preferences.getPreferences(context, 'privacy');
                await prefs.put('privacy_agreed', true);
                await prefs.put('privacy_agreed_time', Date.now());
                await prefs.flush();

                console.info('[Privacy] 用户已同意隐私声明');
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .width('90%')
        .padding(20)
        .backgroundColor('#fff')
        .borderRadius(16)
        .shadow({ radius: 20, color: '#40000000' })
      }
    }
    .width('100%')
    .height('100%')
  }
}

export { PrivacyComplianceKit };

四、踩坑与注意事项

4.1 隐私声明必须在功能之前展示

:应用先进入主界面,再弹隐私声明——审核直接被拒。

:隐私声明必须在应用启动时、用户使用任何功能之前展示。用户点击"同意"后才能继续。

4.2 权限被拒绝后的降级处理

:用户拒绝权限后,应用直接崩溃或功能完全不可用。

:每个敏感权限都应该有替代方案。相机被拒绝?让用户从相册选择。位置被拒绝?让用户手动输入城市。

// ✅ 正确:权限被拒后提供替代方案
async function getLocation(): Promise<string> {
  const granted = await requestPermission('ohos.permission.LOCATION');
  if (granted) {
    return await getCurrentLocation();
  } else {
    // 替代方案:让用户手动选择城市
    return await showCityPicker();
  }
}

4.3 广告ID的限制追踪检查

:不检查限制广告追踪状态就直接使用广告ID,导致隐私合规问题。

:每次使用广告ID前,必须先检查 isLimitAdTrackingEnabled()。如果用户限制了追踪,不能使用任何标识符替代。

4.4 数据删除不能"留尾巴"

:用户请求数据删除,但只删了本地数据,服务端和备份中的数据没删。

:数据删除必须覆盖所有存储位置——本地、服务端、备份、CDN 缓存。建议建立删除检查清单。

4.5 敏感数据不能明文存储

:将身份证号、密码等敏感数据明文存在 Preferences 中。

:使用 cipherFramework 进行加密存储,密钥使用系统安全存储(HUKS)。


五、HarmonyOS 6 适配

5.1 API 变化一览

变化项 HarmonyOS 5 HarmonyOS 6
隐私看板 基础权限记录 新增实时权限使用指示器(状态栏图标)
广告ID identifier 模块 新增 OAID(开放广告标识符)合规要求
数据删除 无标准接口 新增 DataDeletionRequest 标准化接口
权限申请 requestPermissionsFromUser 新增"仅本次授权"选项
隐私声明 无标准格式 新增 PrivacyStatement 标准模板
生物识别 基础指纹 新增面部识别隐私保护

5.2 迁移指南

// HarmonyOS 5:自定义数据删除流程
async function handleDeletion() {
  // 自行实现删除逻辑
  await deleteLocalData();
  await callServerDeletionAPI();
}

// HarmonyOS 6:使用标准化接口
import { privacy } from '@kit.AbilityKit';
const deletionManager = privacy.createDataDeletionManager();
await deletionManager.submitDeletionRequest({
  dataType: privacy.DeletionDataType.ALL,
  includeCloudData: true,  // 同时删除云端数据
  includeBackups: true,    // 同时删除备份数据
});

5.3 注意事项

  1. “仅本次授权”:HarmonyOS 6 新增的授权选项,应用退出后自动撤销。需要在每次启动时重新申请。
  2. OAID 合规:如果应用使用广告ID,必须提供"限制广告追踪"的入口,并在用户限制后停止使用。
  3. 实时指示器:系统会在状态栏显示应用正在使用敏感权限的图标,开发者无需额外处理,但应确保权限使用行为合理。

六、总结

mindmap
  root((隐私设置))
    隐私看板
      权限使用概览
      敏感行为记录
      风险提醒
      实时指示器(HarmonyOS 6)
    权限使用记录
      使用时间/时长
      访问频率
      前后台状态
      透明化展示
    广告ID管理
      获取广告ID
      限制广告追踪检查
      广告ID使用记录
      OAID 合规
    数据删除请求
      分级删除选项
      全链路删除
      删除确认机制
      标准化接口(HarmonyOS 6)
    隐私合规开发
      最小权限原则
      透明度原则
      数据最小化
      安全加密存储
      隐私声明弹窗
      降级替代方案
    注意事项
      隐私声明先于功能
      权限拒绝需降级
      广告ID检查限制追踪
      数据删除不留尾巴
      敏感数据加密存储
知识点 要点
隐私看板 记录每次权限使用,让用户看到"谁在用什么"
权限使用记录 记录时间、频率、前后台状态,确保透明度
广告ID 使用前检查限制追踪状态,尊重用户选择
数据删除 覆盖本地/服务端/备份全链路,不留尾巴
最小权限 能不用就不用,能降级就降级,每个权限都有替代方案
HarmonyOS 6 仅本次授权、标准化删除接口、实时权限指示器

隐私保护就像给用户的数据上锁——不是因为你信不过别人,而是因为这是对用户最基本的尊重。在 HarmonyOS 生态中,隐私合规不是可选项,而是必修课。掌握了这些,你的应用才能赢得用户的信任,走得更远。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。