HarmonyOS APP开发中权限声明

📌 核心要点:权限声明是鸿蒙应用安全的第一道防线,通过 module.json5 中的 requestPermissions 声明所需权限,理解 system_grant 与 user_grant 的区别、APL 级别划分,是构建安全应用的基础。


一、背景与动机

想象一下这个场景:你刚搬进一个新小区,小区有健身房、游泳池、地下车库等各种设施。但并不是每个住户都能随意使用所有设施——你需要先在物业那里登记,申请相应的门禁卡,才能进入对应的区域。

鸿蒙系统的权限管理机制,本质上就是这样一个"物业登记"系统。你的应用想要访问用户的相册、摄像头、位置等敏感资源?没问题,但得先"声明"——告诉系统和用户:“我需要这些权限,这是我的理由。”

如果你不声明权限就直接调用相关 API,系统会毫不留情地给你甩一个错误。就像你没办门禁卡就硬闯健身房,保安(系统)直接拦住你,连门都进不去。

那为什么要把权限声明放在 module.json5 里呢? 因为这是一种"前置声明"机制——在应用安装时,系统就能扫描这个配置文件,知道你的应用到底需要哪些权限。这比运行时才发现应用在偷偷干坏事要好得多。用户在安装前就能看到权限列表,做出知情决策。


二、核心原理

2.1 权限声明的工作流程

当你在 module.json5 中声明权限后,系统会根据权限类型走不同的授权流程:

system_grant

user_grant

允许

拒绝

应用在 module.json5 声明权限

权限授签方式?

系统自动授权

安装时即获得权限

应用可直接使用

需要用户手动授权

运行时弹窗请求

用户选择?

获得权限

未获得权限

需要处理拒绝逻辑

2.2 两种授权方式:system_grant vs user_grant

这是权限声明中最核心的概念区分,必须理解透彻。

system_grant(系统授权):这类权限不涉及用户隐私,系统在应用安装时自动授予。比如网络访问权限——你的应用要联网,这不需要用户额外确认,装上就能用。

user_grant(用户授权):这类权限涉及用户隐私数据,必须由用户亲自确认。比如相机权限——你的应用要拍照,用户得点头同意才行。

打个比方:system_grant 就像小区的公共通道,住户自动拥有通行权;user_grant 则像邻居家的门,你得敲门,主人同意了才能进。

2.3 APL 级别:权限的"安全等级"

APL(Ability Privilege Level)是权限的等级标签,决定了哪些应用有资格申请该权限:

APL 级别 说明 典型权限
normal 普通权限,所有应用可申请 ohos.permission.INTERNET
system_basic 基础系统权限,系统应用或特权应用可申请 ohos.permission.LOCATION
system_core 核心系统权限,仅系统核心应用可申请 ohos.permission.INSTALL_BUNDLE

同时,应用自身也有 APL 级别(在 AppScope 中的 app.json5 里配置),应用的 APL 必须 ≥ 权限的 APL,才能成功申请该权限。就像你的职级必须达到某个等级,才能进入对应的会议室。

2.4 module.json5 中的 requestPermissions 结构

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

各字段含义:

  • name:权限名称,必须是系统定义的合法权限名
  • reason:申请理由,对 user_grant 权限必填,向用户解释为什么需要该权限
  • usedScene:使用场景,描述权限在哪些 Ability 中、何时使用
    • abilities:使用该权限的 Ability 列表
    • when:使用时机,inuse(仅前台使用)或 always(前后台都使用)

三、代码实战

示例1:基础权限声明配置

这是一个完整的 module.json5 配置,展示了常见权限的声明方式:

// module.json5 - 模块配置文件
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone", "tablet"],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": ["entity.system.home"],
            "actions": ["action.system.home"]
          }
        ]
      }
    ],
    // ====== 权限声明区域 ======
    "requestPermissions": [
      {
        // 网络权限 - system_grant,安装即授权
        "name": "ohos.permission.INTERNET"
      },
      {
        // 相机权限 - user_grant,需要用户手动授权
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        // 位置权限 - user_grant,需要用户手动授权
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        // 大概位置权限 - user_grant,精度较低的位置
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        // 文件读取权限 - user_grant
        "name": "ohos.permission.READ_MEDIA",
        "reason": "$string:read_media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

对应的字符串资源文件(string.json):

// base/element/string.json
{
  "string": [
    {
      "name": "camera_reason",
      "value": "用于拍摄照片上传头像"
    },
    {
      "name": "location_reason",
      "value": "用于获取您的位置信息,提供附近服务推荐"
    },
    {
      "name": "read_media_reason",
      "value": "用于读取相册图片,方便您选择和分享"
    }
  ]
}

示例2:运行时校验权限声明是否生效

在应用启动时,我们可以通过 Ability 的 onWindowStageCreate 回调来检查权限状态,确保声明已正确配置:

// EntryAbility.ets - 应用入口Ability
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  // 应用创建回调
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    console.info('[PermissionDemo] EntryAbility onCreate');
  }

  // 窗口阶段创建回调
  onWindowStageCreate(windowStage: window.WindowStage): void {
    console.info('[PermissionDemo] EntryAbility onWindowStageCreate');

    // 检查已声明的权限状态
    this.checkDeclaredPermissions();

    // 设置主窗口内容
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        console.error('[PermissionDemo] Failed to load content: ' + JSON.stringify(err));
        return;
      }
      console.info('[PermissionDemo] Succeeded in loading content');
    });
  }

  /**
   * 检查已声明权限的授权状态
   * 通过此方法可以验证 module.json5 中的权限声明是否生效
   */
  private async checkDeclaredPermissions(): Promise<void> {
    try {
      // 获取访问控制管理器
      const atManager = abilityAccessCtrl.createAtManager();
      
      // 获取当前应用的Bundle名
      const bundleInfo = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
      );
      const bundleName = bundleInfo.name;
      
      // 定义需要检查的权限列表(与 module.json5 中声明的一致)
      const declaredPermissions: Permissions[] = [
        'ohos.permission.INTERNET',
        'ohos.permission.CAMERA',
        'ohos.permission.LOCATION',
        'ohos.permission.APPROXIMATELY_LOCATION',
        'ohos.permission.READ_MEDIA'
      ];

      console.info('[PermissionDemo] 开始检查权限声明状态...');

      // 逐个检查权限授权状态
      for (const permission of declaredPermissions) {
        try {
          const grantStatus = await atManager.checkAccessToken(
            bundleInfo.appInfo.accessTokenId,
            permission
          );
          
          const statusText = grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED 
            ? '已授权 ✅' 
            : '未授权 ❌';
          console.info(`[PermissionDemo] ${permission}: ${statusText}`);
        } catch (err) {
          console.error(`[PermissionDemo] 检查权限 ${permission} 失败: ${JSON.stringify(err)}`);
        }
      }
    } catch (error) {
      console.error('[PermissionDemo] 权限检查异常: ' + JSON.stringify(error));
    }
  }

  onDestroy(): void {
    console.info('[PermissionDemo] EntryAbility onDestroy');
  }
}

示例3:权限声明验证工具页面

创建一个可视化页面,展示当前应用所有已声明权限的状态信息,方便开发调试:

// pages/PermissionCheckPage.ets - 权限声明验证页面
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';

// 权限信息接口
interface PermissionInfo {
  name: string;           // 权限名称
  grantMode: string;      // 授权方式
  isGranted: boolean;     // 是否已授权
  aplLevel: string;       // APL级别
  description: string;    // 权限描述
}

@Entry
@Component
struct PermissionCheckPage {
  // 权限列表状态
  @State permissionList: PermissionInfo[] = [];
  // 加载状态
  @State isLoading: boolean = true;
  // 已授权数量
  @State grantedCount: number = 0;

  // 需要检查的权限列表
  private readonly CHECK_PERMISSIONS: Array<{
    name: Permissions;
    grantMode: string;
    aplLevel: string;
    description: string;
  }> = [
    {
      name: 'ohos.permission.INTERNET',
      grantMode: 'system_grant',
      aplLevel: 'normal',
      description: '网络访问权限'
    },
    {
      name: 'ohos.permission.CAMERA',
      grantMode: 'user_grant',
      aplLevel: 'normal',
      description: '相机权限'
    },
    {
      name: 'ohos.permission.LOCATION',
      grantMode: 'user_grant',
      aplLevel: 'system_basic',
      description: '精确位置权限'
    },
    {
      name: 'ohos.permission.APPROXIMATELY_LOCATION',
      grantMode: 'user_grant',
      aplLevel: 'system_basic',
      description: '大概位置权限'
    },
    {
      name: 'ohos.permission.READ_MEDIA',
      grantMode: 'user_grant',
      aplLevel: 'system_basic',
      description: '读取媒体文件权限'
    },
    {
      name: 'ohos.permission.WRITE_MEDIA',
      grantMode: 'user_grant',
      aplLevel: 'system_basic',
      description: '写入媒体文件权限'
    }
  ];

  // 页面即将显示时检查权限
  async aboutToAppear() {
    await this.checkAllPermissions();
  }

  /**
   * 检查所有已声明权限的状态
   */
  private async checkAllPermissions(): Promise<void> {
    try {
      const atManager = abilityAccessCtrl.createAtManager();
      const bundleInfo = await bundleManager.getBundleInfoForSelf(
        bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
      );
      const tokenId = bundleInfo.appInfo.accessTokenId;

      const results: PermissionInfo[] = [];
      let granted = 0;

      for (const perm of this.CHECK_PERMISSIONS) {
        try {
          const status = await atManager.checkAccessToken(tokenId, perm.name);
          const isGranted = status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
          if (isGranted) granted++;

          results.push({
            name: perm.name,
            grantMode: perm.grantMode,
            isGranted: isGranted,
            aplLevel: perm.aplLevel,
            description: perm.description
          });
        } catch (err) {
          // 权限检查失败,可能未声明该权限
          results.push({
            name: perm.name,
            grantMode: perm.grantMode,
            isGranted: false,
            aplLevel: perm.aplLevel,
            description: perm.description + '(未声明或检查失败)'
          });
        }
      }

      this.permissionList = results;
      this.grantedCount = granted;
      this.isLoading = false;
    } catch (error) {
      console.error('[PermissionCheck] 检查权限失败: ' + JSON.stringify(error));
      this.isLoading = false;
    }
  }

  build() {
    Column() {
      // 标题区域
      Text('权限声明验证工具')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 8 })

      // 统计信息
      Text(`已授权: ${this.grantedCount} / ${this.permissionList.length}`)
        .fontSize(16)
        .fontColor('#666666')
        .margin({ bottom: 16 })

      // 加载中提示
      if (this.isLoading) {
        LoadingProgress()
          .width(48)
          .height(48)
          .color('#4CAF50')
      } else {
        // 权限列表
        List({ space: 12 }) {
          ForEach(this.permissionList, (item: PermissionInfo) => {
            ListItem() {
              this.PermissionItemBuilder(item)
            }
          }, (item: PermissionInfo) => item.name)
        }
        .width('100%')
        .layoutWeight(1)
        .padding({ left: 16, right: 16 })

        // 刷新按钮
        Button('重新检查')
          .width('80%')
          .height(44)
          .backgroundColor('#4CAF50')
          .fontColor(Color.White)
          .margin({ top: 16, bottom: 24 })
          .onClick(() => {
            this.isLoading = true;
            this.checkAllPermissions();
          })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  /**
   * 单个权限项的UI构建
   */
  @Builder
  PermissionItemBuilder(item: PermissionInfo) {
    Row() {
      // 授权状态图标
      Text(item.isGranted ? '✅' : '❌')
        .fontSize(20)
        .margin({ right: 12 })

      // 权限信息
      Column() {
        Text(item.description)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(item.name)
          .fontSize(12)
          .fontColor('#999999')
          .margin({ top: 4 })
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        // 标签行
        Row({ space: 8 }) {
          // 授权方式标签
          Text(item.grantMode === 'system_grant' ? '系统授权' : '用户授权')
            .fontSize(11)
            .fontColor(Color.White)
            .backgroundColor(item.grantMode === 'system_grant' ? '#4CAF50' : '#FF9800')
            .borderRadius(4)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })

          // APL级别标签
          Text(item.aplLevel)
            .fontSize(11)
            .fontColor(Color.White)
            .backgroundColor(item.aplLevel === 'normal' ? '#2196F3' : '#9C27B0')
            .borderRadius(4)
            .padding({ left: 6, right: 6, top: 2, bottom: 2 })
        }
        .margin({ top: 6 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .shadow({ radius: 2, color: '#1A000000', offsetY: 1 })
  }
}

四、踩坑与注意事项

坑1:reason 字段不填或填错导致审核被拒

现象:user_grant 类型的权限声明中,reason 字段是必填的。如果你不填,或者填的内容过于笼统(比如"用于应用功能"),应用市场审核大概率会被打回。

正确做法:reason 必须具体说明权限用途,比如"用于拍摄照片上传头像"比"用于拍照"更清晰。同时,reason 要引用字符串资源($string:xxx),不能直接写硬编码字符串。

// ❌ 错误:直接硬编码
{
  "name": "ohos.permission.CAMERA",
  "reason": "拍照"  // 不符合规范
}

// ✅ 正确:引用字符串资源
{
  "name": "ohos.permission.CAMERA",
  "reason": "$string:camera_reason"  // 规范写法
}

坑2:APL 级别不匹配导致权限申请失败

现象:你的应用 APL 是 normal,但你声明了一个 system_basic 级别的权限(如 ohos.permission.LOCATION),结果安装时该权限不会被授予。

解决方案:在 app.json5 中确认应用的 APL 级别。普通三方应用的 APL 默认是 normal,只能申请 normal 级别的权限。要申请 system_basic 权限,需要通过应用市场签名或者使用调试证书。

// app.json5 - 应用级配置
{
  "app": {
    "bundleName": "com.example.myapp",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name",
    // 注意:三方应用无法直接修改此字段
    // 需要通过签名工具或应用市场配置
    "apiReleaseType": "Release"
  }
}

坑3:LOCATION 和 APPROXIMATELY_LOCATION 必须同时声明

现象:如果你只声明了 ohos.permission.LOCATION(精确位置),而没有声明 ohos.permission.APPROXIMATELY_LOCATION(大概位置),运行时请求位置权限会失败。

原因:鸿蒙的位置权限设计要求,精确位置权限必须以大概位置权限为基础。也就是说,你必须"先有大范围,才能精确到点"。

// ❌ 错误:只声明精确位置
"requestPermissions": [
  {
    "name": "ohos.permission.LOCATION",
    "reason": "$string:location_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
  }
]

// ✅ 正确:同时声明两个位置权限
"requestPermissions": [
  {
    "name": "ohos.permission.APPROXIMATELY_LOCATION",
    "reason": "$string:location_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
  },
  {
    "name": "ohos.permission.LOCATION",
    "reason": "$string:location_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
  }
]

坑4:usedScene.when 字段选择不当

现象:你的应用只需要在前台使用位置,但你声明了 "when": "always",审核时可能被质疑权限过度申请。

原则:最小权限原则——只在必要时申请必要范围的权限。如果只需要前台使用,就填 inuse;只有确实需要后台持续定位(如导航应用),才填 always

坑5:权限名称拼写错误

现象:权限名称写错了,比如把 ohos.permission.CAMERA 写成了 ohos.permission.Camera(大小写错误),编译不会报错,但运行时权限永远拿不到。

建议:直接从官方文档复制权限名称,不要手动输入。权限名称是区分大小写的!


五、HarmonyOS 6 适配

5.1 权限声明格式变化

HarmonyOS 6 对 requestPermissions 的校验更加严格:

变化项 HarmonyOS 5 HarmonyOS 6
reason 字段 建议填写 强制校验,不填直接编译警告
usedScene 可选 user_grant 权限必须填写
权限最小化 建议实践 编译时检查权限冗余,过度声明会警告
新增权限 - 新增 AI 相关权限(如 ohos.permission.AI_VOICE

5.2 迁移指南

// HarmonyOS 6 推荐的完整权限声明格式
"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "$string:camera_reason",           // 必填
    "usedScene": {                                 // 必填
      "abilities": ["EntryAbility"],              // 指定使用的Ability
      "when": "inuse"                             // 明确使用时机
    }
  }
]

5.3 HarmonyOS 6 新增的权限分级

HarmonyOS 6 引入了更细粒度的权限分级,部分权限拆分为"基础"和"增强"两个层级:

  • ohos.permission.READ_MEDIA → 拆分为 ohos.permission.READ_MEDIA(基础读取)和 ohos.permission.READ_MEDIA_V2(含元数据读取)
  • 新增 ohos.permission.SENSORS 传感器权限组,替代原来分散的各传感器权限

六、总结

权限声明知识图谱
├── 声明位置
│   └── module.json5 → requestPermissions 数组
├── 授权方式
│   ├── system_grant → 安装即授权(如 INTERNET)
│   └── user_grant → 需用户确认(如 CAMERA)
├── APL 级别
│   ├── normal → 所有应用可申请
│   ├── system_basic → 系统应用可申请
│   └── system_core → 核心系统应用专属
├── 声明字段
│   ├── name → 权限名称(区分大小写)
│   ├── reason → 申请理由(user_grant 必填)
│   └── usedScene → 使用场景(abilities + when)
└── 最佳实践
    ├── 最小权限原则
    ├── reason 具体明确
    ├── 位置权限成对声明
    └── when 字段按需选择

核心记忆口诀

  1. 声明在前,使用在后——先在 module.json5 声明,才能在代码中请求
  2. system 自动,user 手动——system_grant 自动授权,user_grant 需要运行时请求
  3. APL 匹配,等级够才行——应用 APL 必须 ≥ 权限 APL
  4. reason 要具体,when 要精确——权限理由和使用场景都不能含糊
  5. 位置成对,大小写对——LOCATION 和 APPROXIMATELY_LOCATION 一起声明,权限名称严格区分大小写

权限声明看似只是配置文件里几行 JSON,但它是整个权限管理体系的基石。声明错了,后面的动态申请、权限校验全都是空中楼阁。把这一步做扎实,后面的路才能走得稳。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐