鸿蒙 HarmonyOS 6 | 系统能力 (01):权限金字塔 ATM 模型与动态申请权限
从 module.json5 的静态声明,到运行时的动态申请,再到被拒绝后的二次引导,每一个环节都是为了保护用户的隐私安全。作为开发者,我们不应将此视为阻碍,而应将其视为建立用户信任的契机。
前言
在移动应用开发的蛮荒时代,获取用户隐私就像是自助餐一样随意,应用安装时列出一长串清单,用户只要想用就必须全盘接受。但随着隐私安全成为操作系统的底线,这种“一揽子买卖”早已成为历史。
在鸿蒙 HarmonyOS 6 (API 20) 中,安全机制被提升到了前所未有的高度。系统引入了 ATM (Access Token Manager) 机制,构建了一座森严的“权限金字塔”。
对于开发者而言,这意味着我们不能再理所当然地调用相机、读取通讯录或者获取定位。每一个敏感操作之前,都必须经过一道严格的安检。
很多刚上手的开发者会被 module.json5 里的配置搞得晕头转向,或者在处理动态权限申请的回调地狱中苦不堪言。特别是当我们面对 system_basic 这种系统级权限时,往往会因为签名问题碰壁。
今天,我们就来彻底拆解鸿蒙的权限体系,并亲手封装一个 Promise 化的权限请求工具,让繁琐的授权流程变得像调用普通函数一样简单。

一、 权限金字塔:分级与 ATM 模型
鸿蒙的权限管理核心是 ATM 模型,它将权限划分为了不同的等级,形成了一个金字塔结构。位于塔底的是 system_grant(系统授权) 类型的权限,比如访问网络(ohos.permission.INTERNET)。这类权限只要我们在配置文件里声明了,应用安装时系统就会自动授予,不需要用户感知,属于“默认通行证”。
真正的挑战在于塔尖的 user_grant(用户授权) 类型。这包括相机、麦克风、位置、日历等涉及用户核心隐私的权限。对于这类权限,单纯的静态声明是不够的,我们必须在代码运行时,显式地向用户发起申请,系统会弹出一个标准化的授权弹窗,只有用户点击“允许”,我们才能拿到通行令牌(Token)。
此外,还有一个让很多开发者困惑的概念是 APL (Available Privilege Level),也就是应用权限等级。鸿蒙将应用分为 normal、system_basic 和 system_core 三个等级。普通的第三方应用默认是 normal 等级。如果你想申请一些高级权限(比如拦截骚扰电话、管理系统窗口),这些权限往往被标记为 system_basic。对于普通开发者来说,这意味着即便你声明了这些权限,系统也不会给你。
除非你的应用通过了特殊的签名证书配置(ACL)或者你是系统预置应用,否则这些高级权限就是禁区。所以,在设计功能时,务必先查阅文档,确认你所需的权限是否对 normal 等级开放。
二、 静态声明:module.json5 的门票
一切权限申请的起点都是 module.json5 文件。这里是我们向系统递交的“申请书”。在 requestPermissions 字段中,我们需要列出应用所需的所有权限。
这里有一个极易踩坑的细节:reason(申请理由)。对于 user_grant 类型的权限,reason 字段是必填的,而且它必须是一个 资源引用($r),不能是硬编码的字符串。系统在弹出授权框时,会展示这个 reason 里的文案,告诉用户你为什么要用相机。如果你的理由写得含糊不清,比如“需要使用相机”,用户很可能会拒绝;而如果你写“需要使用相机扫描二维码进行支付”,通过率就会大大提高。此外,usedScene 字段虽然是可选的,但建议填写,它可以声明权限使用的场景(比如是前台使用还是后台使用),这对于通过应用市场的审核至关重要。
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:mic_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
三、 动态申请:告别回调地狱
在完成了静态声明后,重头戏来了:动态申请。原生的 abilityAccessCtrl API 虽然功能强大,但调用起来略显繁琐。我们需要先获取 AtManager 实例,然后构建请求数组,最后调用 requestPermissionsFromUser。这个方法是异步的,原本是基于回调的设计,如果在业务逻辑中层层嵌套,代码的可读性会非常差。
我们需要的是一个 Promise 化 的封装。想象一下,我们希望在点击按钮时,只需要写一行 await PermissionManager.request('ohos.permission.CAMERA'),如果用户同意就继续执行,拒绝就抛出异常或提示。这样的代码才符合现代异步编程的审美。
为了实现这个目标,我们需要封装一个单例的 PermissionManager。在这个类中,我们不仅要处理申请逻辑,还要处理 权限校验 逻辑。因为用户可能会在设置中心手动关闭权限,所以每次使用功能前,我们都应该先通过 checkAccessToken 检查当前的授权状态。如果已经是 PERMISSION_GRANTED,就直接放行;如果是 PERMISSION_DENIED,才发起弹窗申请。
四、 优雅的二次确认与引导
还有一个用户体验的痛点:如果用户点击了“禁止”并且勾选了“不再提示”,系统自带的弹窗就再也不会出现了。这时候如果不做处理,用户点击按钮会没有任何反应,以为程序坏了。
一个成熟的权限工具类,应该具备 二次引导 的能力。当 requestPermissionsFromUser 返回的结果显示用户拒绝了授权,我们需要弹出一个自定义的 Dialog,委婉地告诉用户:“我们需要这个权限才能拍照,请去设置中心开启。”并提供一个按钮,直接跳转到系统的应用设置页。这才是完整的闭环。
下面是一个完整的权限管理工具类封装。它包含了检查权限、申请权限以及处理用户拒绝后的引导逻辑。你可以直接将这个 PermissionManager 复制到你的项目中,用它来替换掉那些散落在各处的原生 API 调用。
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
// -------------------------------------------------------------
// 1. 权限管理单例封装
// -------------------------------------------------------------
class PermissionManager {
private static instance: PermissionManager;
private atManager: abilityAccessCtrl.AtManager;
private constructor() {
this.atManager = abilityAccessCtrl.createAtManager();
}
public static getInstance(): PermissionManager {
if (!PermissionManager.instance) {
PermissionManager.instance = new PermissionManager();
}
return PermissionManager.instance;
}
/**
* 检查是否已授予权限
* @param permission 权限名称
*/
async checkPermission(permission: Permissions): Promise<boolean> {
try {
const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
const appInfo = bundleInfo.appInfo;
const tokenId = appInfo.accessTokenId;
const grantStatus = await this.atManager.checkAccessToken(tokenId, permission);
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch (error) {
console.error('Check permission failed:', error);
return false;
}
}
/**
* 核心方法:请求权限 (Promise 化)
* @param context UIAbilityContext
* @param permissions 权限数组
* @returns boolean 是否全部授权成功
*/
async requestPermissions(context: common.UIAbilityContext, permissions: Permissions[]): Promise<boolean> {
try {
// 1. 先发起系统弹窗请求
const result = await this.atManager.requestPermissionsFromUser(context, permissions);
// 2. 检查请求结果
// result.authResults 数组对应 permissions 数组的授权状态
const isAllGranted = result.authResults.every(status => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);
if (isAllGranted) {
return true;
} else {
// 3. 如果被拒绝,可以在这里统一处理引导逻辑
// 比如检测到用户勾选了"不再提示",则弹出自定义弹窗引导去设置页
// 这里为了简化,只返回 false
return false;
}
} catch (error) {
const err = error as BusinessError;
console.error(`Request permissions failed, code: ${err.code}, message: ${err.message}`);
return false;
}
}
/**
* 引导用户去设置页开启权限
* 当用户勾选"不再提示"导致无法弹出授权框时调用
*/
openSystemSettings(context: common.UIAbilityContext) {
const want: common.Want = {
bundleName: 'com.huawei.hmos.settings',
abilityName: 'com.huawei.hmos.settings.MainAbility',
uri: 'application_info_entry', // 跳转到应用详情页
parameters: {
pushParams: 'com.example.your_bundle_name' // 替换为你的包名
}
};
context.startAbility(want).catch((err: BusinessError) => {
console.error('Open settings failed:', err);
});
}
}
// 导出单例
export const permissionManager = PermissionManager.getInstance();
// -------------------------------------------------------------
// 2. 界面实战:相机权限申请
// -------------------------------------------------------------
@Entry
@Component
struct PermissionDemoPage {
@State isCameraGranted: boolean = false;
// 核心:在页面加载时检查状态
async checkStatus() {
this.isCameraGranted = await permissionManager.checkPermission('ohos.permission.CAMERA');
}
aboutToAppear(): void {
this.checkStatus();
}
build() {
Column() {
// 顶部说明
Text('权限管理实战')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 40, bottom: 20 })
// 状态展示
Row() {
Text('相机权限状态:')
.fontSize(16)
Text(this.isCameraGranted ? '已授权' : '未授权')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(this.isCameraGranted ? '#0A59F7' : '#FF4040')
}
.margin({ bottom: 40 })
// 模拟相机功能区域
if (this.isCameraGranted) {
Column() {
Stack() {
Rect().width(200).height(200).fill('#333').radius(16)
Image($r('app.media.app_icon')).width(50).height(50) // 模拟取景框
Text('相机取景中...').fontColor(Color.White).margin({ top: 80 })
}
}
.transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
} else {
// 未授权时的占位图
Column() {
Text('权限被拒绝,无法使用相机')
.fontColor('#999')
.margin({ bottom: 20 })
Button('申请相机权限')
.onClick(async () => {
// 获取上下文
const context = getContext(this) as common.UIAbilityContext;
// 发起申请
const granted = await permissionManager.requestPermissions(context, ['ohos.permission.CAMERA']);
if (granted) {
this.isCameraGranted = true;
promptAction.showToast({ message: '授权成功' });
} else {
// 如果被拒绝,弹出自定义引导
promptAction.showDialog({
title: '权限申请',
message: '我们需要相机权限来提供拍照功能,请在设置中开启。',
buttons: [
{ text: '取消', color: '#666666' },
{
text: '去设置',
color: '#0A59F7',
primary: true
}
]
}).then((resp) => {
if (resp.index === 1) {
// 跳转设置页
permissionManager.openSystemSettings(context);
}
});
}
})
}
}
}
.width('100%')
.height('100%')
.backgroundColor('#F1F3F5')
}
}
五、 总结
鸿蒙的权限体系虽然严格,但逻辑非常清晰。从 module.json5 的静态声明,到运行时的动态申请,再到被拒绝后的二次引导,每一个环节都是为了保护用户的隐私安全。作为开发者,我们不应将此视为阻碍,而应将其视为建立用户信任的契机。
更多推荐




所有评论(0)