HarmonyOS APP开发中权限声明
HarmonyOS APP开发中权限声明
📌 核心要点:权限声明是鸿蒙应用安全的第一道防线,通过 module.json5 中的 requestPermissions 声明所需权限,理解 system_grant 与 user_grant 的区别、APL 级别划分,是构建安全应用的基础。
一、背景与动机
想象一下这个场景:你刚搬进一个新小区,小区有健身房、游泳池、地下车库等各种设施。但并不是每个住户都能随意使用所有设施——你需要先在物业那里登记,申请相应的门禁卡,才能进入对应的区域。
鸿蒙系统的权限管理机制,本质上就是这样一个"物业登记"系统。你的应用想要访问用户的相册、摄像头、位置等敏感资源?没问题,但得先"声明"——告诉系统和用户:“我需要这些权限,这是我的理由。”
如果你不声明权限就直接调用相关 API,系统会毫不留情地给你甩一个错误。就像你没办门禁卡就硬闯健身房,保安(系统)直接拦住你,连门都进不去。
那为什么要把权限声明放在 module.json5 里呢? 因为这是一种"前置声明"机制——在应用安装时,系统就能扫描这个配置文件,知道你的应用到底需要哪些权限。这比运行时才发现应用在偷偷干坏事要好得多。用户在安装前就能看到权限列表,做出知情决策。
二、核心原理
2.1 权限声明的工作流程
当你在 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 字段按需选择
核心记忆口诀:
- 声明在前,使用在后——先在 module.json5 声明,才能在代码中请求
- system 自动,user 手动——system_grant 自动授权,user_grant 需要运行时请求
- APL 匹配,等级够才行——应用 APL 必须 ≥ 权限 APL
- reason 要具体,when 要精确——权限理由和使用场景都不能含糊
- 位置成对,大小写对——LOCATION 和 APPROXIMATELY_LOCATION 一起声明,权限名称严格区分大小写
权限声明看似只是配置文件里几行 JSON,但它是整个权限管理体系的基石。声明错了,后面的动态申请、权限校验全都是空中楼阁。把这一步做扎实,后面的路才能走得稳。
更多推荐



所有评论(0)