HarmonyOS开发:NEXT版权限模型——新权限体系

举报
Jack20 发表于 2026/06/27 20:33:53 2026/06/27
【摘要】 HarmonyOS开发:NEXT版权限模型——新权限体系📌 核心要点:NEXT版权限模型从"安装时全量授权"变为"运行时按需申请+分级授权+用户可控撤回",权限粒度更细,申请流程更严,你的App必须重新设计权限策略。 背景与动机你有没有遇到过这种情况——用户安装你的App,一看权限列表:位置、相机、麦克风、通讯录、存储……十几个权限一口气弹出来,用户直接点"拒绝"或者干脆卸载。V5的权限...

HarmonyOS开发:NEXT版权限模型——新权限体系

📌 核心要点:NEXT版权限模型从"安装时全量授权"变为"运行时按需申请+分级授权+用户可控撤回",权限粒度更细,申请流程更严,你的App必须重新设计权限策略。

背景与动机

你有没有遇到过这种情况——用户安装你的App,一看权限列表:位置、相机、麦克风、通讯录、存储……十几个权限一口气弹出来,用户直接点"拒绝"或者干脆卸载。

V5的权限模型有个大问题:很多敏感权限在安装时就自动授予了,用户根本不知道。等用户发现App在后台偷偷用位置、读通讯录的时候,信任已经崩了。

NEXT版彻底改了这套机制。核心变化就一句话:用户说了算

什么意思?敏感权限必须运行时申请,用户可以逐个授权或拒绝,已经授权的权限随时可以撤回,而且App不能反复弹窗骚扰用户。这套机制对用户友好,但对开发者来说——你得重新设计整个权限申请流程。

核心原理

NEXT版权限分级体系

先搞清楚NEXT版的权限分几级,不同级别的申请策略完全不同:

graph TB
    classDef normal fill:#2ecc71,stroke:#27ae60,color:#fff,stroke-width:2px
    classDef system fill:#f39c12,stroke:#e67e22,color:#fff,stroke-width:2px
    classDef user fill:#e74c3c,stroke:#c0392b,color:#fff,stroke-width:2px
    classDef restricted fill:#9b59b6,stroke:#8e44ad,color:#fff,stroke-width:2px

    A[NEXT权限分级] --> B[normal<br/>普通权限]:::normal
    A --> C[system_basic<br/>系统基础权限]:::system
    A --> D[user_grant<br/>用户授权权限]:::user
    A --> E[restricted<br/>受限权限]:::restricted

    B --> B1[安装时自动授予<br/>如:网络访问]:::normal
    C --> C1[系统签名App才能申请<br/>如:系统设置修改]:::system
    D --> D1[运行时弹窗申请<br/>如:相机、位置]:::user
    E --> E1[仅系统App可用<br/>如:底层硬件访问]:::restricted
权限级别 授权方式 示例 能否撤回
normal 安装时自动授予 INTERNET、GET_NETWORK_INFO 不能
system_basic 系统签名App才能申请 SET_TIME、CONNECTIVITY_INTERNAL 不能
user_grant 运行时弹窗申请 CAMERA、LOCATION、MICROPHONE 可以
restricted 仅系统App MANAGE_USB、INSTALL_BUNDLE 不适用

和V5的核心区别在哪?

  1. V5的很多user_grant权限在安装时默认授予,NEXT全部改为运行时申请
  2. NEXT新增了权限使用说明——申请权限时必须告诉用户为什么需要这个权限
  3. NEXT支持权限的精确控制——比如位置权限可以只授权"大致位置"而非"精确位置"
  4. NEXT支持单次授权——用户可以选择"仅本次允许"

新增权限与废弃权限

变化类型 权限 说明
新增 ohos.permission.APP_TRACKING_CONSENT 跨应用追踪需要用户明确同意
新增 ohos.permission.READ_IMAGEVIDEO 替代旧的存储权限,精确到图片/视频
新增 ohos.permission.READ_AUDIO 精确到音频文件
新增 ohos.permission.READ_DOCUMENT 精确到文档文件
废弃 ohos.permission.READ_MEDIA 拆分为READ_IMAGEVIDEO/READ_AUDIO
废弃 ohos.permission.WRITE_MEDIA NEXT不再支持写媒体库,用安全保存
变更 ohos.permission.LOCATION 新增精确/大致位置选项

权限申请流程变化

V5和NEXT的权限申请流程对比:

graph LR
    classDef v5 fill:#e74c3c,stroke:#c0392b,color:#fff,stroke-width:2px
    classDef next fill:#2ecc71,stroke:#27ae60,color:#fff,stroke-width:2px

    subgraph V5流程
        V1[安装App]:::v5 --> V2[安装时自动<br/>授予部分权限]:::v5
        V2 --> V3[运行时弹窗<br/>申请敏感权限]:::v5
        V3 --> V4[用户选择<br/>允许/拒绝]:::v5
        V4 --> V5[拒绝后可<br/>反复弹窗]:::v5
    end

    subgraph NEXT流程
        N1[安装App]:::next --> N2[仅授予normal<br/>权限]:::next
        N2 --> N3[使用功能时<br/>按需申请权限]:::next
        N3 --> N4[展示权限<br/>使用说明]:::next
        N4 --> N5[用户选择<br/>允许/仅本次/拒绝]:::next
        N5 --> N6[拒绝后不可<br/>反复弹窗]:::next
        N6 --> N7[引导用户到<br/>设置页面]:::next
    end

关键差异:

  1. V5安装时自动授予部分权限NEXT只授予normal权限
  2. V5拒绝后可以反复弹窗NEXT拒绝后不能反复弹,只能引导到设置
  3. NEXT新增"仅本次允许"选项 → 用户可以临时授权,App退出后自动撤回
  4. NEXT新增权限使用说明 → 申请权限时必须展示reason

代码实战

基础用法:运行时权限申请

NEXT版的标准权限申请流程:

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

// 需要申请的权限列表
const REQ_PERMISSIONS: Permissions[] = [
  'ohos.permission.CAMERA',
  'ohos.permission.READ_IMAGEVIDEO'
];

@Entry
@Component
struct PermissionDemo {
  @State cameraGranted: boolean = false;
  @State mediaGranted: boolean = false;

  async aboutToAppear() {
    // 先检查已有权限
    await this.check_permissions();
  }

  /**
   * 检查权限状态
   */
  async check_permissions(): Promise<void> {
    const atManager = abilityAccessCtrl.createAtManager();
    const bundleInfo = await bundleManager.getBundleInfoForSelf(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
    );

    this.cameraGranted = await this.check_single(
      atManager, bundleInfo.appInfo.accessTokenId, 'ohos.permission.CAMERA'
    );
    this.mediaGranted = await this.check_single(
      atManager, bundleInfo.appInfo.accessTokenId, 'ohos.permission.READ_IMAGEVIDEO'
    );
  }

  private async check_single(
    atManager: abilityAccessCtrl.AtManager,
    tokenId: number,
    permission: Permissions
  ): Promise<boolean> {
    try {
      const status = await atManager.checkAccessToken(tokenId, permission);
      return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
    } catch {
      return false;
    }
  }

  /**
   * 申请权限——NEXT版标准流程
   */
  async request_permissions(): Promise<void> {
    const atManager = abilityAccessCtrl.createAtManager();

    try {
      // NEXT版:requestPermissionsFromUser会弹出系统权限对话框
      // 对话框中会显示你在module.json5中配置的reason
      const result = await atManager.requestPermissionsFromUser(
        getContext(this),
        REQ_PERMISSIONS
      );

      // 处理每个权限的授权结果
      for (let i = 0; i < result.authResults.length; i++) {
        const granted = result.authResults[i] === 0;
        const perm = REQ_PERMISSIONS[i];
        console.info(`权限 ${perm}: ${granted ? '已授权' : '被拒绝'}`);

        if (perm === 'ohos.permission.CAMERA') {
          this.cameraGranted = granted;
        } else if (perm === 'ohos.permission.READ_IMAGEVIDEO') {
          this.mediaGranted = granted;
        }
      }
    } catch (err) {
      console.error(`权限申请异常: ${JSON.stringify(err)}`);
    }
  }

  build() {
    Column() {
      Text('权限状态')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Row() {
        Text(`相机: ${this.cameraGranted ? '✅' : '❌'}`)
          .fontSize(18)
        Text(`媒体: ${this.mediaGranted ? '✅' : '❌'}`)
          .fontSize(18)
          .margin({ left: 20 })
      }
      .margin({ top: 20 })

      Button('申请权限')
        .margin({ top: 20 })
        .onClick(() => this.request_permissions())
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

进阶用法:权限被拒后的处理

NEXT版权限被拒后不能反复弹窗,需要引导用户到设置页面手动开启:

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

/**
 * NEXT版权限管理器
 * 封装权限申请、拒绝处理、设置引导
 */
export class NextPermissionManager {
  private context: common.UIAbilityContext;
  private atManager: abilityAccessCtrl.AtManager;

  // 记录权限被拒次数——用于判断是否需要引导到设置
  private deniedCount: Map<string, number> = new Map();

  constructor(context: common.UIAbilityContext) {
    this.context = context;
    this.atManager = abilityAccessCtrl.createAtManager();
  }

  /**
   * 申请权限——带拒绝处理
   */
  async request_with_fallback(
    permissions: Permissions[],
    reason: string
  ): Promise<boolean> {
    // 1. 先检查是否已授权
    const allGranted = await this.check_all(permissions);
    if (allGranted) {
      return true;
    }

    // 2. 申请权限
    try {
      const result = await this.atManager.requestPermissionsFromUser(
        this.context, permissions
      );

      let allOk = true;
      for (let i = 0; i < result.authResults.length; i++) {
        const granted = result.authResults[i] === 0;
        const perm = permissions[i];

        if (!granted) {
          allOk = false;
          // 记录拒绝次数
          const count = (this.deniedCount.get(perm) || 0) + 1;
          this.deniedCount.set(perm, count);

          console.warn(`权限 ${perm} 被拒绝 (第${count}次)`);

          // NEXT版:拒绝2次以上,引导到设置页面
          if (count >= 2) {
            this.show_settings_dialog(perm, reason);
          }
        } else {
          this.deniedCount.delete(perm); // 授权后清除拒绝记录
        }
      }

      return allOk;
    } catch (err) {
      console.error(`权限申请异常: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 引导用户到设置页面
   */
  private show_settings_dialog(permission: string, reason: string): void {
    // NEXT版:弹出AlertDialog引导用户去设置
    // 注意:这里不能直接跳转设置,需要用户确认
    console.info(`建议引导用户到设置页面开启权限: ${permission}`);
    console.info(`权限用途: ${reason}`);

    // 使用UIContext弹出对话框
    // 实际项目中应该用AlertDialog
  }

  /**
   * 跳转到应用设置页面
   */
  async open_app_settings(): Promise<void> {
    // NEXT版:通过隐式Want跳转到应用设置
    const want = {
      action: 'action.settings.app.info',
      parameters: {
        bundleName: this.context.abilityInfo.bundleName
      }
    };

    try {
      await this.context.startAbility(want);
    } catch (err) {
      console.error(`跳转设置失败: ${JSON.stringify(err)}`);
    }
  }

  /**
   * 检查所有权限是否已授权
   */
  private async check_all(permissions: Permissions[]): Promise<boolean> {
    const bundleInfo = await bundleManager.getBundleInfoForSelf(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
    );

    for (const perm of permissions) {
      const status = await this.atManager.checkAccessToken(
        bundleInfo.appInfo.accessTokenId, perm
      );
      if (status !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
        return false;
      }
    }
    return true;
  }
}

完整示例:按需权限申请框架

把权限申请、拒绝处理、使用说明整合成完整框架:

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

/**
 * 权限配置项
 */
interface PermissionConfig {
  permission: Permissions;
  reason: string;           // 权限使用说明——NEXT版必须提供
  required: boolean;        // 是否为必须权限(拒绝则功能不可用)
  fallbackMessage: string;  // 拒绝后的提示信息
}

/**
 * NEXT版权限申请框架
 * 按需申请、拒绝处理、设置引导一体化
 */
export class PermissionFramework {
  private context: common.UIAbilityContext;
  private atManager: abilityAccessCtrl.AtManager;
  private configs: PermissionConfig[] = [];

  constructor(context: common.UIAbilityContext) {
    this.context = context;
    this.atManager = abilityAccessCtrl.createAtManager();
  }

  /**
   * 注册权限配置
   */
  register(config: PermissionConfig): void {
    this.configs.push(config);
  }

  /**
   * 批量注册权限配置
   */
  registerAll(configs: PermissionConfig[]): void {
    configs.forEach(c => this.configs.push(c));
  }

  /**
   * 按功能申请权限
   * 只申请该功能需要的权限,不一次性申请所有权限
   */
  async request_for_feature(featureName: string): Promise<boolean> {
    // 找到该功能需要的权限
    const featurePerms = this.configs.filter(c =>
      c.reason.includes(featureName) || c.required
    );

    if (featurePerms.length === 0) {
      console.warn(`未找到功能 ${featureName} 的权限配置`);
      return true;
    }

    // 先检查已授权的
    const needRequest: Permissions[] = [];
    for (const config of featurePerms) {
      const granted = await this.check_permission(config.permission);
      if (!granted) {
        needRequest.push(config.permission);
      }
    }

    if (needRequest.length === 0) {
      return true; // 所有权限已授权
    }

    // 申请缺失的权限
    try {
      const result = await this.atManager.requestPermissionsFromUser(
        this.context, needRequest
      );

      let allGranted = true;
      for (let i = 0; i < result.authResults.length; i++) {
        const granted = result.authResults[i] === 0;
        const perm = needRequest[i];
        const config = featurePerms.find(c => c.permission === perm);

        if (!granted && config?.required) {
          allGranted = false;
          console.warn(`必须权限 ${perm} 被拒绝: ${config.fallbackMessage}`);
        } else if (!granted) {
          console.info(`可选权限 ${perm} 被拒绝,功能可能受限`);
        }
      }

      return allGranted;
    } catch (err) {
      console.error(`权限申请异常: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 检查单个权限
   */
  async check_permission(permission: Permissions): Promise<boolean> {
    try {
      const bundleInfo = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
      );
      const status = await this.atManager.checkAccessToken(
        bundleInfo.appInfo.accessTokenId, permission
      );
      return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
    } catch {
      return false;
    }
  }

  /**
   * 获取所有权限状态
   */
  async get_all_status(): Promise<Map<string, boolean>> {
    const statusMap = new Map<string, boolean>();
    for (const config of this.configs) {
      const granted = await this.check_permission(config.permission);
      statusMap.set(config.permission, granted);
    }
    return statusMap;
  }
}

// ===== 使用示例 =====
@Entry
@Component
struct PermissionFrameworkDemo {
  private permFramework: PermissionFramework | null = null;
  @State cameraReady: boolean = false;
  @State locationReady: boolean = false;

  aboutToAppear() {
    this.permFramework = new PermissionFramework(
      getContext(this) as common.UIAbilityContext
    );

    // 注册权限配置——NEXT版必须提供reason
    this.permFramework.registerAll([
      {
        permission: 'ohos.permission.CAMERA',
        reason: '拍照功能需要使用相机',
        required: true,
        fallbackMessage: '没有相机权限,拍照功能无法使用'
      },
      {
        permission: 'ohos.permission.APP_TRACKING_CONSENT',
        reason: '个性化推荐需要跨应用追踪',
        required: false,
        fallbackMessage: '没有追踪权限,推荐内容可能不够精准'
      },
      {
        permission: 'ohos.permission.LOCATION',
        reason: '附近功能需要获取您的位置',
        required: false,
        fallbackMessage: '没有位置权限,无法使用附近功能'
      },
      {
        permission: 'ohos.permission.READ_IMAGEVIDEO',
        reason: '选择图片功能需要读取相册',
        required: true,
        fallbackMessage: '没有相册权限,无法选择图片'
      }
    ]);
  }

  // 点击拍照按钮时才申请相机权限
  async onTakePhoto() {
    if (!this.permFramework) return;
    const granted = await this.permFramework.request_for_feature('拍照');
    this.cameraReady = granted;
    if (granted) {
      console.info('可以开始拍照');
    }
  }

  // 点击附近按钮时才申请位置权限
  async onNearby() {
    if (!this.permFramework) return;
    const granted = await this.permFramework.request_for_feature('附近');
    this.locationReady = granted;
    if (granted) {
      console.info('可以加载附近内容');
    }
  }

  build() {
    Column() {
      Text('按需权限申请')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Button('📷 拍照')
        .margin({ top: 20 })
        .enabled(!this.cameraReady)
        .onClick(() => this.onTakePhoto())

      Button('📍 附近')
        .margin({ top: 10 })
        .enabled(!this.locationReady)
        .onClick(() => this.onNearby())

      if (this.cameraReady) {
        Text('相机权限已就绪 ✅')
          .fontSize(14)
          .fontColor('#2ecc71')
          .margin({ top: 10 })
      }
      if (this.locationReady) {
        Text('位置权限已就绪 ✅')
          .fontSize(14)
          .fontColor('#2ecc71')
          .margin({ top: 5 })
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

踩坑与注意事项

1. module.json5中必须声明reason

NEXT版要求所有user_grant权限在module.json5中声明reason,否则编译报错:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",  // 必填!
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"  // inuse(使用时)或 always(始终)
        }
      }
    ]
  }
}

2. READ_MEDIA拆分为三个权限

V5的ohos.permission.READ_MEDIA在NEXT中拆分为:

  • ohos.permission.READ_IMAGEVIDEO——读取图片和视频
  • ohos.permission.READ_AUDIO——读取音频
  • ohos.permission.READ_DOCUMENT——读取文档

如果你只需要读取图片,不要申请READ_AUDIO和READ_DOCUMENT——按需申请。

3. 位置权限的精确/大致选项

NEXT版位置权限支持两种精度:

  • ohos.permission.LOCATION——精确位置(GPS级别)
  • ohos.permission.APPROXIMATELY_LOCATION——大致位置(城市级别)

用户可以选择只授权大致位置。你的App需要处理"只有大致位置"的情况。

// 检查是否有精确位置
const hasExactLocation = await check_permission('ohos.permission.LOCATION');
const hasApproxLocation = await check_permission('ohos.permission.APPROXIMATELY_LOCATION');

if (hasExactLocation) {
  // 使用精确位置
} else if (hasApproxLocation) {
  // 使用大致位置——功能降级
  console.warn('只有大致位置权限,定位精度受限');
} else {
  // 没有位置权限
}

4. 权限被拒后不能反复弹窗

NEXT版限制了权限弹窗的频率。用户拒绝后,短时间内再次调用requestPermissionsFromUser不会弹出对话框,而是直接返回拒绝结果。

正确做法:拒绝后引导用户到设置页面手动开启,而不是反复弹窗。

5. 后台位置需要两个权限

如果你的App需要在后台获取位置,需要同时申请ohos.permission.LOCATIONohos.permission.LOCATION_IN_BACKGROUND。而且后台位置权限的审核更严格——只有导航类、运动健康类App才能通过。

6. 单次授权的处理

NEXT新增了"仅本次允许"选项。用户选择后,App退出时权限自动撤回。你的App需要处理"权限突然消失"的情况——每次使用功能前都要检查权限,不能缓存权限状态。

HarmonyOS 6适配说明

HarmonyOS 6在NEXT权限模型的基础上,新增了以下特性:

  1. 权限使用审计:6.0新增权限使用记录,用户可以查看每个App何时使用了哪个权限
  2. 权限自动过期:6.0支持权限设置有效期,过期后自动撤回
  3. 最小权限推荐:6.0的DevEco Studio新增权限审查工具,自动检测过度申请的权限
  4. 隐私沙箱:6.0引入隐私沙箱机制,某些权限(如广告追踪)在沙箱中执行,App无法获取原始数据

升级到6.0后,建议关注权限使用审计的适配,以及隐私沙箱对广告和追踪功能的影响。

总结

NEXT版权限模型的核心变化:从"安装时全量授权"到"运行时按需申请"

这对用户是好事——他们可以精确控制每个权限。但对开发者来说,你必须重新设计权限申请流程:不能用"一口气申请所有权限"的粗暴方式,而是要按功能、按场景、按需申请。

记住三条铁律:

  1. 按需申请——用到才申请,不提前申请
  2. 给理由——每个权限都要告诉用户为什么需要
  3. 优雅降级——权限被拒后功能降级,不能直接崩溃
维度 评价
学习难度 ⭐⭐⭐ 流程不复杂但细节多
使用频率 ⭐⭐⭐⭐⭐ 每个涉及敏感功能的App都要处理
重要程度 ⭐⭐⭐⭐⭐ 权限处理不当,App直接被拒审

一句话:NEXT的权限模型是"用户说了算",你必须按需申请、给理由、优雅降级。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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