资讯详情

资讯详情

鸿蒙权限金字塔ATM模型与动态申请权限实战指南

做鸿蒙应用开发这两年我发现自己和周围朋友在权限这块翻车的次数比在其他任何系统能力上都多。权限声明写了弹窗就是不出现用户明明点了允许下一次打开还是提示没有权限代码里已经过了权限校验进到相机界面却黑屏——这些问题追到最后几乎都指向同一个根源对鸿蒙 HarmonyOS 6 的权限金字塔 ATM 模型理解得不够透。这篇文章就围绕权限金字塔 ATM 模型和动态申请权限把整套体系从概念到代码拆开讲顺便把我自己踩过的坑一并排掉。文章会从 APL 等级开始接着拆 ATM 的令牌机制然后给你完整的动态申请权限链路最后用拍照场景做一次全流程演示适合正在做应用开发、或者想系统理解鸿蒙权限体系的同学参考。1. 权限金字塔到底在说什么APL 等级、授权类型与 ACL和很多系统把权限做成权限清单不同鸿蒙把权限设计成了一个分层模型。每次有人问我这个权限到底能不能动态申请这个权限我能不能用,我基本都会先让他把权限金字塔的三层结构画一遍。画清楚了一大半疑问就消失了。1.1 从塔底到塔尖normal、system_basic 与 system_core鸿蒙里每个应用在安装时都会有一个应用权限等级也就是 APLAbility Privilege Level。这个等级决定了应用能申请哪些权限从低到高分三档normal普通应用默认等级绝大多数第三方应用都属于这一档。日常用到的相机、麦克风、定位、媒体库读取等权限都是这一档可以触达的。system_basic需要系统签名或通过特殊渠道申请才能拿到普通应用默认不在此列。涉及设备配置、系统设置类能力大多在这里。system_core塔尖只有系统核心进程或被明确允许的少数场景才能持有普通应用基本不用考虑。我习惯把这三档想象成一个小区的门禁系统normal 是业主卡能进出自己家和小区公共区域system_basic 是物业卡能打开水电井、电梯机房system_core 是总控卡整个小区的核心设备房都能进。开发者日常打交道的是最底下这一层往上每一层都对应严格得多的管控。注意APL 是应用等级不是权限等级。某个权限本身可能被标记为normal 等级可申请但应用自己也得有对应的 APL 才能申。反过来一个 system_basic 的应用去申请 normal 权限完全没问题只是没必要。为了方便对照我把三档等级放在一起看APL 等级面向对象一般获取方式典型能力范围normal普通应用安装时或运行时网络、相机、麦克风、定位、媒体库等system_basic系统级应用系统签名 / ACL系统设置、设备管理、部分系统信息读取system_core核心系统组件仅系统权限直接持有内核级操作、系统环境关键能力这个金字塔最直观的价值是当你在文档里看到一个权限第一反应应该是去查它所属的 APL 等级和授权方式而不是先去看这个功能能不能做。1.2 system_grant 与 user_grant两种授权时机APL 决定你能不能申请某个权限而授权方式决定系统什么时候把权限给你。鸿蒙把权限分为两类system_grant系统授权应用安装时由系统自动授予不需要用户参与弹窗。典型代表有ohos.permission.INTERNET网络访问、ohos.permission.GET_NETWORK_INFO获取网络信息等。这类权限用户甚至不会在任何权限页看到开关因为安装阶段就直接下发了。user_grant用户授权必须在运行时向用户发起动态申请用户点击允许后系统才会真正下发。相机、麦克风、精确位置、媒体库读取都属于这一类。这也是动态申请权限这个词绝大多数时候所指的对象。这里有一个常见误解很多人以为动态申请是指代码里调一次 API 就能拿到权限。实际上代码只是触发了系统的授权弹窗真正的授权动作发生在系统设置页里最终决定权在用户手上。你在代码里能做的是发起申请、拿到用户选择的回调结果、根据结果切换业务逻辑。为了体现授权时机的差异我把两类权限的几个常见例子放一起授权类型典型权限名称授权时机用户可见度system_grantohos.permission.INTERNET安装时自动授予不可见system_grantohos.permission.GET_WIFI_INFO安装时自动授予不可见user_grantohos.permission.CAMERA运行时弹窗申请可见可管user_grantohos.permission.MICROPHONE运行时弹窗申请可见可管user_grantohos.permission.ACCESS_FINE_LOCATION运行时弹窗申请可见可管1.3 ACL给普通应用打开一条向上申请的口子APL 是金字塔那普通应用是不是永远只能待在塔底也不完全是。鸿蒙提供了一个名为 ACLAccess Control List访问控制列表的机制某些原本属于 system_basic 等级的权限如果业务确实需要允许 normal 等级应用通过 ACL 方式申请。ACL 申请有几个特点第一需要在上架或分发平台登记受限权限声明说明申请原因和使用场景第二审核人员会核实你的应用是不是真的离不开这个权限第三通过后系统会以受限使用的方式下发能力而不是像真正的 system_basic 应用那样放开所有同类权限。我自己的一个教训是刚开始做某个内部工具应用时图省事直接申请了一个 system_basic 权限结果被审核打回了一次理由是使用场景不明确。后来把申请理由改成仅在用户主动触发时读取设备标识用于故障诊断并附上了触发页面路径才顺利通过。提示ACL 不是随便点了就能生效的。开发调试阶段可以在设备上做一些临时授权但正式发布必须走完整的审核链路。审核不通过的话即便代码里写了 AC 在部分系统版本上也不会真正获得能力运行时会直接报权限校验失败。所以能不用 ACL 就不 ACL业务设计时应优先考虑把功能落在 normal 权限能覆盖的范围内。2. ATM 模型核心拆解AccessToken 如何被创建、查询与校验APL 和授权方式是权限的规则面而 ATMAccessToken Manager访问令牌管理器是权限的执行面。每次应用调用系统能力时系统怎么知道这个应用有没有权限答案就落在令牌这两个字上。2.1 ATM 在整条系统链路里的位置鸿蒙的权限体系可以抽象成三层应用层、框架层、系统服务层。应用层就是你的 HAP/APP 代码框架层负责把权限申请动作翻译成系统调用系统服务层运行着一个常驻的权限管理服务也就是 ATM 所在的位置。应用安装时包管理服务会把应用的签名信息、APL 等级、声明的权限列表打包发送给 ATMATM 校验通过后生成一个对应的访问令牌AccessToken。之后应用每次调用需要权限的接口系统服务会先拿着应用的 AccessToken 去 ATM 查一次这个应用什么等级、申请过哪些权限、目标权限当前状态如何。查完才决定放行还是拦下。这个过程用生活类比就是小区业主入住时先在物业登记了车牌和房间号物业给了一张门禁卡。后面每次刷卡进楼门禁系统都读一次卡核对你是不是这栋楼的业主、你的门禁权限有没有被暂停。ATM 就是那个物业系统AccessToken 就是那张门禁卡。2.2 三个关键标识UserID、TokenID 与 AppID在 ATM 的视角里光有应用身份还不够因为同一个应用可能运行在不同用户空间下。真正参与权限校验的标识有三个UserID用户 ID标记是哪个用户在操作。鸿蒙支持多用户空间同一台设备上的不同用户各自的权限状态是互相隔离的。TokenID访问令牌 IDATM 在应用启动后分配相当于门禁卡编号。权限校验时主要用这个 ID 去查权限状态。AppID应用 ID由包名和签名信息共同决定同一个应用重装后签名变了 AppID 也会变。实际开发中绝大多数情况下你不需要主动构造这些 ID而是从上下文对象里直接取。比如在 UIAbility 的页面里可以通过getContext(this).accessTokenId拿到当前应用的 TokenID后面做显式权限校验时会用到。多用户切换导致的权限异常、权限状态神秘失效很多时候就是 TokenID 和 UserID 的绑定关系变了。2.3 权限校验的两条路径系统服务在调用链路上对权限的校验是隐式完成的——你在代码里调用相机接口如果没权限框架层会直接给你抛异常。这对开发者来说是省事的因为大部分权限问题会在运行时报错时暴露出来。但如果你的业务需要在 UI 上提前展示有权限/没权限的状态就需要在代码里做一次显式校验。鸿蒙提供了verifyAccessToken接口import { abilityAccessCtrl, common } from kit.AbilityKit; import { hilog } from kit.PerformanceAnalysisKit; // 获取当前应用的 accessTokenId const context getContext(this) as common.UIAbilityContext; const tokenId context.accessTokenId; // 创建 ATM 管理器 const atManager abilityAccessCtrl.createAtManager(); // 显式校验相机权限 const grantStatus atManager.verifyAccessToken( tokenId, ohos.permission.CAMERA ); if (grantStatus abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { hilog.info(0x0000, Demo, Camera permission granted); } else { hilog.warn(0x0000, Demo, Camera permission denied); }这里要注意verifyAccessToken返回的是当前权限状态的一次快照。如果用户刚在系统设置里把权限关掉下一次校验结果会立刻反映出来反过来如果用户刚授予权限但你的页面还保留着旧的校验结果UI 展示就可能出现已经授权了却还显示未授权的现象。所以显式校验最好放在真正需要执行敏感操作的节点上而不是在页面加载时查一次就缓存到底。3. 动态申请权限完整实操从声明到弹窗再到回调理论说完开始上手。动态申请权限的完整链路可以分成三步先在配置文件里声明权限然后调用申请 API最后处理用户授权结果。每一步都有讲究尤其是第一步很多人都栽在只写代码不写声明上。3.1 module.json5 里先把声明写对先看一个典型的module.json5权限声明{ module: { name: entry, type: entry, requestPermissions: [ { name: ohos.permission.CAMERA, reason: $string:reason_camera, usedScene: { abilities: [EntryAbility], when: inuse } }, { name: ohos.permission.ACCESS_FINE_LOCATION, reason: $string:reason_location, usedScene: { abilities: [EntryAbility, LocationAbility], when: inuse } } ] } }三个关键字段必须搞清楚name权限名称必须是系统定义好的ohos.permission.XXX形式不能自定义拼错任何一个字母都会导致运行时权限不存在。reason申请理由用于授权弹窗里向用户解释为什么需要这个权限。实际发布时应用商店审核也会重点核对这个理由和业务场景是否匹配。usedScene使用场景需要声明在哪个 Ability 里使用以及使用时机是inuse使用时还是always后台持续。always类的声明审核更严格能不用就不用。经验reason不要写成提升用户体验这种空话。填用于扫码识别二维码就比用于提升功能完整性通过率高得多。这是审核的硬要求也是用户是否愿意授权的关键影响因子。还有一个容易忽略的点requestPermissions里只声明还不够运行时还需要真正调用申请接口。也就是说声明和申请是两步声明解决这个应用请求哪些权限申请解决用户现在同不同意。3.2 requestPermissionsFromUser 的调用细节运行时发起动态申请核心 API 是requestPermissionsFromUser。一个完整的调用长这样import { abilityAccessCtrl, common, Permissions } from kit.AbilityKit; import { BusinessError } from kit.BasicServicesKit; async function requestPermission() { const context getContext(this) as common.UIAbilityContext; const atManager abilityAccessCtrl.createAtManager(); const permissions: ArrayPermissions [ ohos.permission.CAMERA, ohos.permission.ACCESS_FINE_LOCATION ]; try { const result await atManager.requestPermissionsFromUser( context, permissions ); // result.permissions 与 result.authResults 按顺序一一对应 result.authResults.forEach((grantStatus, index) { if (grantStatus abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) { console.info(Permission ${result.permissions[index]} granted); } else { console.warn(Permission ${result.permissions[index]} denied); } }); } catch (err) { // 请求过程本身失败比如上下文失效、服务异常 const error err as BusinessError; console.error(Request permission failed, code: ${error.code}, message: ${error.message}); } }几个容易踩的细节authResults数组的顺序和传入的permissions数组顺序是严格对应的不能只判断第 0 个元素否则有多个权限时容易漏判。建议一次申请属于同一类业务场景的权限比如进贴纸拍照页时同时申请相机和相册。如果一次性申请五六个不相关权限弹窗会被系统拆成几个页面逐组展示体验很差授权率也会直线下降。如果用户之前已经拒绝过某个权限再次调用这个 API 时系统不会依赖缓存自动通过而是会结合实际状态返回结果。对于拒绝且不再询问的情况弹窗甚至不会出现回调里直接返回拒绝。3.3 用户点拒绝之后怎么办动态申请里最考验产品设计的就是拒绝分支。很多开发者在用户拒绝后只会弹一个 Toast然后就没有然后了。但用户一旦误点拒绝后续功能就永久不可用除非用户自己找到系统设置页去手动打开。正确的做法是检测到拒绝后给出引导入口带着用户跳转到系统设置的应用权限管理页。这里我给一段跳转代码import { Want, common } from kit.AbilityKit; import { BusinessError } from kit.BasicServicesKit; function openPermissionSettings(context: common.UIAbilityContext) { const want: Want { action: action.settings.app.detail, parameters: { bundleName: com.example.myapplication } }; context.startAbility(want) .then(() { console.info(Open settings success); }) .catch((err: BusinessError) { console.error(Open settings failed, code: ${err.code}, message: ${err.message}); }); }这里有一个我不止一次踩到的坑action的取值在不同系统版本上有差别而且部分定制系统的设置页路径不完全一致。我的建议是保留这段代码的同时在页面里叠加一个明显的文字按钮去授权告诉用户请在设置-应用-权限管理中打开相机权限两条路都走才不会把用户卡死。4. 走一个完整 Demo拍照功能的权限申请全链路理论再完整不落到代码里都记不牢。这一节我拿一个随手拍页面做例子完整走一遍进入页面先查权限 → 没有权限就动态申请 → 拿到权限后调用相机能力 → 如果遇到拒绝就引导去设置页。4.1 场景设定与事前检查这个单页应用的业务逻辑很简单页面正中放一个按钮点击后打开相机拍照。相机的ohos.permission.CAMERA是 user_grant 权限所以必须走动态申请。在点击事件里直接调申请接口是最直观的写法但我更推荐在页面初次可见时先做一次静默检查。好处有两个一是可以提前在界面上展示拍照权限未开启的提示条让用户有心理预期二是避免用户第一次点按钮时被突如其来的弹窗打断操作。事前检查逻辑import { abilityAccessCtrl, common } from kit.AbilityKit; function isPermissionGranted(permission: string): boolean { const context getContext(this) as common.UIAbilityContext; const tokenId context.accessTokenId; const atManager abilityAccessCtrl.createAtManager(); const status atManager.verifyAccessToken(tokenId, permission); return status abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; }4.2 封装一个权限管理器为了避免在页面代码里到处散落createAtManager和回调处理我通常会封装一个小工具类。这对于多个页面共用权限场景尤其有用import { abilityAccessCtrl, common, Permissions } from kit.AbilityKit; import { BusinessError } from kit.BasicServicesKit; export class PermissionManager { private static instance: PermissionManager | null null; static getInstance(): PermissionManager { if (!PermissionManager.instance) { PermissionManager.instance new PermissionManager(); } return PermissionManager.instance; } async checkPermission(permission: string): Promiseboolean { const context getContext(this) as common.UIAbilityContext; const tokenId context.accessTokenId; const atManager abilityAccessCtrl.createAtManager(); const status atManager.verifyAccessToken(tokenId, permission); return status abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; } async requestPermission( context: common.UIAbilityContext, permission: Permissions ): Promiseboolean { const atManager abilityAccessCtrl.createAtManager(); try { const result await atManager.requestPermissionsFromUser( context, [permission] ); return result.authResults[0] abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; } catch (err) { const error err as BusinessError; console.error(Request permission failed: ${error.code}, ${error.message}); return false; } } }单例的好处是无论页面跳转多少次ATM 管理器只要创建一次权限判断逻辑也收敛在一个文件里。后续如果权限多起来还能在这个工具类里扩展批量申请被永久拒绝时自动弹引导等能力。4.3 页面里怎么用以及运行观察点页面代码用的是 ArkUI 的声明式写法。核心思路是按钮点击时先检查权限有就直接进入拍照流程没有就先申请申请成功继续申请失败展示引导弹窗。import { common, Permissions } from kit.AbilityKit; import { promptAction } from kit.ArkUI; import { PermissionManager } from ./PermissionManager; Entry Component struct Index { private permissionManager: PermissionManager PermissionManager.getInstance(); async handleTakePhoto() { const context getContext(this) as common.UIAbilityContext; const cameraPermission: Permissions ohos.permission.CAMERA; const granted await this.permissionManager.checkPermission(cameraPermission); if (granted) { // 已授权直接进入相机页面或调起系统相机能力 this.openCamera(); return; } // 未授权发起动态申请 const result await this.permissionManager.requestPermission( context, cameraPermission ); if (result) { this.openCamera(); } else { promptAction.showDialog({ title: 需要相机权限, message: 请在系统设置中开启相机权限否则无法拍照, buttons: [ { text: 取消, color: #666666 }, { text: 去设置, color: #007DFF } ] }).then((value) { if (value.index 1) { this.openPermissionSettings(); } }); } } openCamera() { // 调起相机能力实际项目中可能是跳转到录入页或调用系统相机 console.info(Camera is ready); } openPermissionSettings() { // 参考 3.3 的跳转代码 } build() { Column() { Text(随手拍) .fontSize(24) .fontWeight(FontWeight.Bold) .margin({ top: 24 }); Button(拍照) .width(160) .height(48) .margin({ top: 48 }) .onClick(() { this.handleTakePhoto(); }); } .width(100%) .height(100%) .justifyContent(FlexAlign.Start) } }运行这个 Demo 时建议盯着下面四个状态走一遍覆盖到才算真正掌握动态申请权限的行为场景预期行为观察点第一次安装后进入页面点击按钮弹出授权弹窗弹窗文案里的 reason 是否显示正确用户点击允许相机功能正常打开授权后再次点击按钮不再弹窗用户点击拒绝弹窗关闭业务进入失败分支引导弹窗是否展示去设置入口在系统设置中手动关掉权限回到应用点击按钮重新申请是否被系统识别为可再申请/弹窗行为差异实测下来第一个状态对应的行为最稳定第四个状态最容易出幺蛾子因为 拒绝且不再询问和用户手动在设置里关掉在系统内部被标记成了两种不同的状态后续申请返回的结果也不完全一样。这个细节直接影响了第五章要讲的翻车场景。5. 权限申请翻车现场我踩过的坑与排查思路这一节把我过去几个月在权限上遇到过的问题集中复盘一遍。每个问题都给出可复现的排查链路而不是直接给结论这样下次你遇到类似问题才能自己顺着路子找出答案。5.1 声明了却没有弹窗先查 PermissionName 和模块字段症状代码里明明调了requestPermissionsFromUser设备上却一点反应都没有回调直接返回失败。我的排查顺序是打开module.json5确认requestPermissions里真的写着目标权限且name完全正确。这个字段不能有一点拼写错误比如ohos.permission.CAMERA和ohos.permission.CAMERA大小写不同系统都视为另一个不存在的权限。确认usedScene.abilities覆盖了当前页面所在的 Ability。如果当前页面在EntryAbility但usedScene里只写了OtherAbility弹窗就不会出现。确认reason字段引用的字符串资源存在。如果$string:reason_camera对应的资源没定义部分版本会在解析配置阶段静默失败导致权限列表为空。确认目标权限是user_grant类型。system_grant权限不应该走动态申请强行申请也没有意义系统不会为这类权限弹窗。前两步占了问题的八成以上。尤其是usedScene老工程从低版本升级上来时经常漏补属于典型的看起来配置对了实际上没生效。5.2 用户拒绝了就再也拿不到授权状态机的理解偏差症状用户第一次点了拒绝之后每次进入页面都直接进入失败分支弹窗也不再出现。这不是系统 bug而是权限状态机的正常表现。鸿蒙对用户拒绝的处理分为两个阶段第一次拒绝应用还可以再次发起申请系统会重新弹窗。此时弹窗上可能还会额外提示用户该权限上次已拒绝。多次拒绝或勾选了拒绝且不再询问系统把权限标记为永久拒绝之后requestPermissionsFromUser不会再弹窗而是直接返回拒绝结果。产品上的应对策略是在失败分支里不要反复去调申请接口而是把用户往设置页引导。或者提供一个检查权限状态的按钮让用户在主动操作后再回来继续业务流程。5.3 Context 不对导致的弹窗起不来症状点击按钮后权限弹窗一闪而过甚至完全不出现日志里报context invalid相关错误。这个坑主要出现在把申请逻辑封装到了非页面类里。在鸿蒙里动态申请的context必须是UIAbilityContext或与其相关的能力上下文。如果在纯工具类、数据仓库类、或者globalThis上随便取一个 context 去调用很可能会因为上下文不具备拉起授权弹窗的能力而失败。我的处理习惯是所有需要弹窗的权限申请方法都要显式从调用方传入UIAbilityContext比如上一章封装的PermissionManager.requestPermission(context, permission)。内部再校验一下 context 类型不行就抛错提示调用方式不对避免线上静默失败。5.4 多用户与设备账号切换的隐藏槽点症状同一个应用在设备上切了用户空间或换了访客模式后原本已授权的权限变成未授权且重新申请时弹窗文案也变了。原因就是第二章讲的权限状态和 UserID、TokenID 是绑定的。多用户空间下每个用户的授权记录相互隔离。测试时如果有访客模式工作空间之类的多用户场景务必在权限用例里覆盖一遍否则用户切换账号回来后的流程很可能是断的。此外应用被卸载重装后原有的授权记录也会被清空。如果业务对权限状态做过本地缓存记得在重装后重新走一遍动态申请不能直接拿旧的缓存结果去判断。6. 权限方案设计的一点经验从能用变成好用权限申请不只是技术实现它本质上是一个产品体验和用户信任问题。同一款应用权限弹窗设计得好的授权率可能高出三成以上。这一节聊聊我在项目里沉淀下来的方案设计经验。6.1 最小权限原则怎么在产品里落地最小权限原则挂在嘴上很容易落到设计上需要回答三个问题这个功能真的需要权限吗需要哪个权限能不能晚一点再要我在一个笔记应用里就是这么处理的用户写文字不需要任何敏感权限真正需要图库权限的是插入图片这个动作。所以我没有在启动时申请图库而是在用户点击插入图片按钮的前一瞬才发起申请。这样做的结果有两个启动时的弹窗污染降为零用户对应用的第一印象明显更好。另一个更细的决策是能申请粗粒度权限就不申请细粒度。比如只做天气展示ACCESS_COARSE_LOCATION就够了非要申请ACCESS_FINE_LOCATION反而会让用户警觉也过不了审核的用途审查。6.2 单次授权和仅使用期间允许怎么选新版本系统的授权弹窗里用户其实有三个选项允许、拒绝、仅使用期间允许或类似的一次性授权形式。不同授权选项对应的运行状态不一样仅使用期间允许在应用退到后台一段时间后会自动撤销授权。这对位置类功能影响最大。比如打车软件需要在后台持续上报位置那么申请ACCESS_BACKGROUND_LOCATION类权限时就要接受更严格的审核和使用场景说明而一个只看当前定位的天气应用用仅使用期间允许就完全够用既保护了用户隐私也降低了商店审核时对权限用途的追问概率。6.3 测试阶段怎么快速覆盖各种授权状态权限测试最烦的地方是状态多、组合多。手动去系统设置里反复开关权限不仅慢还容易漏。我建议在测试阶段做到三点写一个内部测试页面专门一键列出当前应用持有的所有权限及授权状态方便回归前快速盘点。在自动化测试中把拒绝授权分支作为独立用例跑一遍不要只测快乐路径否则拒绝引导逻辑很容易变成无人维护的僵尸代码。每次改动权限声明或授权流程后都要在首次安装授权、拒绝后重启应用、设置页手动关闭、多用户切换四组状态下各回归一轮。前三条覆盖的是常见场景最后一条最容易漏但出问题后影响也最隐蔽。我自己在实际操作中的体会是权限方案做得好不好不看代码写得有多漂亮而是看用户在误操作后能不能自己走回来。把拒绝分支做完整把设置页引导做顺滑把授权状态展示准确比多写十个功能接口都更能拉高应用的评价。这套权限金字塔 ATM 模型和动态申请权限的理解都会落到这些真实细节里。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →