鸿蒙权限管理:受限权限申请(六)
搞鸿蒙开发有一段时间的朋友,应该都有这种体会:权限申请本身并不难,难的是搞清楚“哪些权限能直接申请,哪些权限得走特殊流程”。尤其是受限权限,英文文档里叫 restricted permissions,中文社区里讨论得不算多,但凡是做应用市场审核、做系统应用、做企业分发的,迟早都会撞上它。
这篇是《鸿蒙权限管理》系列里比较“硬核”的一篇,专门聊受限权限申请这件事。我会把它的基本逻辑、为什么存在这层限制、申请时和普通权限有什么不一样、实战里最常踩的坑,尽量一次讲透。适合已经写过 requestPermissionsFromUser 但被拒得不明不白,或者马上要面对应用市场审核的开发者。
- 为什么会有“受限权限”这种东西
1.1 权限也分三六九等
鸿蒙的权限体系,按官方文档的划分,大致分成了 normal(普通权限)、system_basic(系统基础权限)、system_core(系统核心权限)这几类。普通权限好理解,比如访问网络、获取网络状态,只要在 module.json5 里声明,然后在运行时向用户申请,用户点了同意就能用。
但有一类权限比较特殊,它在官方权限列表里存在,声明方式也和普通权限一样,但当你运行时去申请,系统会直接拒绝,甚至不会弹窗。这类权限内部被称为 restricted permissions,中文官方翻译是“受限权限”,有的地方也叫“受限申请权限”。
我第一次遇到这个坑,是在做一个文件管理类应用的时候。需要在系统公共目录里创建自己的文件夹,当时按惯性给应用声明了媒体位置的权限,结果运行时申请直接被拒了,错误码返回 201,提示信息模模糊糊,一看就是权限等级不够,但等级哪里不够,API 文档写得并不直白。
后来才搞清楚,受限权限是普通应用能申请的“上限”之外的一类权限。它的特点是:权限声明存在、权限用途明确、系统能力也确实提供,但是出于用户数据安全、设备控制安全等原因,不允许一般应用在未经验证的情况下直接获取。
1.2 谁在限制你,限制的是什么
鸿蒙的权限校验,背后是一套能力开放策略。系统不可能把所有能力都无差别开放给所有应用,否则一个随便下载的 App 就能读取设备上的敏感信息,整个安全模型就崩了。
受限权限限制的核心有三个:
- 权限声明里的级别(level)不够。有些权限在 ACL(访问控制列表)里只允许 system_core 或 system_basic 级别的应用声明,普通应用声明了也白搭。
- 需要额外申请受限权限的授权。这个授权不是运行时的用户弹窗就能解决的,而是需要开发者在应用市场或特定渠道完成受限权限申请,审核通过后,你的应用签名指纹才会被加入白名单。
- 即便代码里调用了申请接口,用户也未收到任何提示。因为受限权限的申请根本不会走用户授权弹窗,它是去“权限管理”界面或系统层面做静态控制的。
这么一解释,你就能理解为什么很多开发者在本地调试时一切正常,一上真机或者一提交审核就出问题。本地调试环境可能默认给了你调试签名对应的权限白名单;真机上,如果你的应用没有通过受限权限申请流程,权限自然拿不到。
- 受限权限和普通权限到底差在哪里
很多教程里会把“敏感权限”和“受限权限”混着讲,实际这俩不是一回事。普通权限里也有敏感的,比如定位、相机、麦克风,它们在运行时会触发用户授权弹窗,用户拒绝的话你再申请一次也还能再弹;但受限权限的敏感程度通常更高,涉及的不只是单个用户的一次授权,而是会影响整个系统设备状态或者跨应用的数据访问边界。
为了不让大家绕晕,我把我在项目里常用的对比表直接放出来。
| 对比项 | 普通权限 | 受限权限 |
|---|---|---|
| 声明方式 | module.json5 里声明 | module.json5 里声明(同) |
| 运行时弹窗 | 有,用户点同意即可 | 一般没有,需要预授权 |
| 授权主体 | 用户 | 用户 + 系统/应用市场侧审核 |
| 权限级别 | normal / system_basic 可覆盖 | 通常 system_basic 起步,core 常见 |
| 调试阶段 | 直接可用 | 往往需要白名单或额外签名配置 |
| 典型例子 | 获取设备网络状态 | 读取外部存储公共目录、后台弹窗、安装未知来源应用等 |
从这个表能清楚看到,如果只是自己写了个 Demo,在 DevEco Studio 的模拟器里跑,因为模拟器默认签名匹配的是调试用的白名单配置,受限权限可能不会触发拦截。但你一旦用 release 签名打包装到用户真机上,问题立刻暴露。
2.1 被 ACL 卡住是什么体验
ACL 在鸿蒙权限体系里全称是 Access Control List,也就是访问控制列表。它本质上是一张“哪些应用能拿哪些权限”的表。你在 module.json5 里写的每一个权限,系统在安装和运行阶段都会拿它跟你应用的身份信息(特别是签名指纹)做比对。
受限权限之所以难搞,是因为它要求你的应用先在“受限权限申请”这个环节里通过审核。审核通过后,系统后台会把你的应用信息加入白名单。这一步如果没过,后面代码写得再漂亮都没有意义。
我遇到过一个真实案例,一个第三方输入法应用需要读取剪贴板历史权限。这个权限本身官方开放给系统输入法场景,第三方应用理论上可以申请,但初版提交审核时因为没在申请材料里写清楚剪贴板数据只在本机处理、不会上传到云端,直接被驳回了。后来补充了隐私政策链接和本地处理的架构说明,才通过。这说明受限权限申请不仅看代码能力,还看你的应用对用户数据的承诺和实际操作。
- 受限权限申请的完整流程
3.1 第一步:先判断你的权限到底是不是受限权限
网上能搜到的最全的权限列表其实就在官方 API 文档里,但那个文档是以接口维度展示的,不是按“受限/不受限”分好类,看起来很累。我在项目里的做法是直接查权限的 level 字段。
当你打开 module.json5,写完一个权限之后,把鼠标悬停在代码提示上,DevEco Studio 通常会直接展示这个权限的等级。如果等级是 system_basic 或 system_core,大概率需要走受限权限申请。如果等级是 normal 但实际申请被拒,再考虑是不是签名和系统版本的原因。
有一种快速验证法:在代码里强行调用 requestPermissionsFromUser,如果返回的 result 里 authResult 对应权限码是 -1 或 201(一般对应IPC/权限校验错误),并且没有弹出系统授权弹窗,那大概率不是普通权限,需要转成受限权限思路来处理。
3.2 第二步:准备申请材料
鸿蒙应用如果要在华为应用市场或者其他正式分发渠道上线,受限权限申请不是直接在 DevEco Studio 里点一下就行的。通常需要登录 AppGallery Connect,在应用的“用户权限”或“API 权限”管理模块里找到受限权限列表,逐个发起申请。
申请时要填的内容一般包括:
- 申请该权限的具体业务场景。比如为什么要读取外部存储的公共目录,实际是在哪个页面、哪个功能流程里使用。
- 使用该权限涉及的数据类型。这里建议把数据是否出境、是否加密存储、是否第三方共享都讲清楚。
- 隐私政策页面链接。用于让审核方确认你确实公开了相应权限的收集使用说明。
- 如果是企业内部分发,不一定走应用市场,但要向设备管理侧提交 APK/HAP 包和签名信息,由企业管理员统一审批白名单。
审题时有一条硬性逻辑:如果你申请的是一个用户可以明显感知的高危权限,但业务场景又交代不出个所以然来,基本上就会被驳回。比如一个阅读器应用去申请后台弹窗权限,解释空间就非常有限,除非它有强提醒类的业务。
我第一次给文件类应用申请“读取公共媒体文件”相关受限权限时,材料里附上了完整的用户操作链截图:用户在主界面点击“导入文件”,系统文件选择器界面跳转到公共目录,用户自行选择文件,应用读取并导入。审核侧看到截图后基本没有追问,一次通过。
3.3 第三步:把代码路径打通
材料提交后的等待期里,代码侧的改造也要同步做。这里有个特别容易犯的错:以为白名单审核通过之后,代码里原来的权限申请逻辑就不用改了。
事实是,受限权限即便有白名单,在端侧运行时,系统依然会做一次权限校验。如果你的应用走的是 requestPermissionsFromUser 的动态申请逻辑,但白名单中的授权方式是 install grant(安装时授权),那么运行时申请不会弹出任何窗口,而是直接返回已授权的结果。这虽然是好消息,但如果你没做防御性判断,只靠弹窗结果来触发后续逻辑,就会有问题:授权成功回调可能走不到你预期的分支,或者因为状态没有正确同步,功能反而没启动。
我建议正式版本里的写法是分层判断:
- 先调用 permissionManager 查询目标权限是否已授予。
- 若已授予,直接进入业务逻辑。
- 若未授予,判断是否为受限权限,是受限权限就引导用户到系统设置页面手动开启;非受限权限再弹动态请求框。
以鸿蒙的 API 为例,检查权限是否授予可以用 @ohos.abilityAccessCtrl 里的 verifyAccessToken 方法。判断结果如果是 PERMISSION_GRANTED,就继续往下走。如果拒绝,再用 @ohos.abilityAccessCtrl 构造 permissionRequestResult 来处理。但这里你大概率会发现,受限权限的拒绝并不会返回明确的原因,它只告诉你个“拒绝”。所以更稳妥的方式是提前去查询这个权限的授权模式,决定是弹权限说明页还是直接跳设置页。
- 真机调试受限权限时,最容易误判的 4 个环节
4.1 误判一:把调试签名当正式环境
前面提过本地调试和正式环境不完全一致。我在项目里碰过的典型表现是:用默认的自动签名调试时,系统会临时给应用签一个调试证书,这个证书往往在系统里被配置了较高的信任级别,所以即使权限不是 normal,也能正常弹窗、正常授权。
但一旦切到 release 签名,这个信任关系不存在了。同一行代码,昨天还能跑通,今天就返回“没有权限”。
这事儿的处理办法不是改代码逻辑,而是要在 DevEco Studio 里确认当前的签名证书是 debug 还是 release。如果要验证受限权限在正式环境下的真实表现,建议创建一个 release 级别的签名配置,安装到专用测试机上跑。不要拿用户真机瞎试,试来试去容易把用户的设备状态搞乱。
4.2 误判二:以为用户拒绝了,其实是系统直接拦截
普通权限被拒,系统至少会弹一次框,用户拒绝后,回调里能拿到“拒绝”的状态。受限权限如果没白名单,很多情况下系统连弹框都不会弹,直接返回失败。因为系统在底层判断你的应用具备不具备申请资格,不具有资格时,根本不会把这次申请请求上抛给用户。
这就导致很多开发者误以为自己的动态申请代码写错了,反反复试验证逻辑,却发现代码没有问题。如果遇到这种情况,先不要怀疑代码,去查看应用的签名信息是否在受限权限白名单内。如果不在,代码再对也没用。
4.3 误判三:把“声明了权限”当成“拥有了权限”
在鸿蒙里,声明权限是第一步,授权才是第二步。普通权限需要用户运行时同意,受限权限恰恰相反:如果你白名单已经授权,即使你不行动态申请,权限可能已经在安装时授权了;但如果你白名单没授权,就算你在代码里反复调用申请接口,也不会成功。
所以要养成一个习惯:做敏感操作前,强制查询一遍 verifyAccessToken 的结果,不要依赖静态的声明变量记状态。权限可能随时被撤销,尤其在系统设置里被用户手动关闭后,应用存活期间如果再想拿到它,必须重新走查询与申请流程。
4.4 误判四:忽略了系统版本差异
这个可能是我个人踩得最深的一个坑。鸿蒙在不同系统版本上的权限列表并不是完全一致的。API 9 时代的一些受限权限,到 API 12 的文档里名字可能变了,也可能并入了其他权限组。举个最简单的例子,涉及位置权限时,旧版本可能只需要申请 ohos.permission.LOCATION,但新版本把后台位置权限拆成了单独的项,要求你必须分开声明、分开申请。
受限权限同理,某个权限在 API 10 里是受限的,API 12 可能开放给了部分系统基础应用,但对第三方应用仍然受限。所以看文档时不能只看一个版本,要把你 targetSdkVersion 对应的权限矩阵单独拎出来核实一遍。最稳妥的办法是直接跑一个小测试 App,把不确定的权限列表循环申请一遍,记录每个权限在当前系统上的返回码,再逐个去核对官方权限列表。
- 实际案例:从申请被拒到成功上线的完整排查
这里拿我做文件管理工具时的一个典型案例完整走一遍,很多朋友说看官方文档看不明白,其实结合案例就好懂了。
那款应用需要在公共下载目录创建一个隐藏的数据交换文件夹,需求上确实涉及系统公共存储的写入。一开始我以为这和 Android 的存储权限逻辑差不多,在 module.json5 里声明了媒体位置相关权限和文件读写权限,并且动态申请,结果发现代码里申请的权限一直处于未授权状态,动态请求窗口迟迟不出现。
排查的第一步,我去查了一遍日志。发现权限校验错误码在系统侧返回的比较模糊,不是一个清晰可定位到“你的签名不在白名单”的提示。然后我把应用日志里出现过的权限名逐一放到官方权限文档里搜,发现那个文件读写的权限等级被列为 system_basic,且官方文档里标注了“受限权限”。
第二步,我检查了自己的签名配置。当时开发用的是自动生成的 debug 签名,所以本地功能一切正常。我又尝试把应用装到同事的普通手机上,对方设备上果然复现了权限拿不到的问题。借此确认了问题不是出在代码逻辑,而是签名信任范围。
第三步,走受限权限申请的正式流程。我把申请材料准备好后,在 AppGallery Connect 后台提交了申请,等审核。审核期间我没有闲着,而是直接在代码里做了兼容降级方案:如果受限权限暂时没下来,业务逻辑里就改为跳转系统的文件选择器,让用户主动选目录,应用只通过 SAF 文件选择器拿访问权,避免直接碰公共目录路径。这个方案虽然和最初需求的设计不完全一样,但保证功能可用,也能给用户一个交代。
审核通过后,我把正式签名的包重新打了一次,安装到测试机验证。第一次安装时系统弹了安装期权限授权提示,所有受限权限在设置里的状态变成了“允许”。功能顺利跑通。
这段经历给我最大的启发是:受限权限申请不要等到提审前才准备。应用设计阶段就该明确哪些能力依赖受限权限,材料能早整理就早整理,毕竟审核是要排队的,而且如果第一次材料不齐被打回,再来一轮,时间成本就大了。
- 受限权限代码实现上的一些实操心得
直接给一段我在工程里验证过的核心代码片段。这段代码解决的是“动态申请后处理受限权限静默授权场景”这问题。
typescript复制import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundleManager';
import common from '@ohos.app.ability.common';
async function checkAndRequestPermission(context: common.UIAbilityContext, permission: Permissions): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
const tokenId = bundleInfo.appInfo.accessTokenId;
const grantStatus = await atManager.verifyAccessToken(tokenId, permission);
if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
// 这里不能直接假设弹窗会出现。对受限权限而言,未授权时多半会直接拒绝或静默失败。
// 所以更可靠的方式是拿到当前权限的授权模式,再决定要不要弹说明页或跳系统设置。
const data = await atManager.requestPermissionsFromUser(context, [permission]);
const authResult = data.authResults[0];
// authResult 为 0 表示授权成功;为 -1 表示用户拒绝;但受限权限也有可能是系统没资格弹窗。
if (authResult === 0) {
return true;
}
// 跳转系统设置,让用户手动开启
await context.startAbility({
bundleName: 'com.huawei.hmos.settings',
abilityName: 'com.huawei.hmos.settings.MainAbility'
});
return false;
}
这段代码里最关键的一点是:我用了 verifyAccessToken 先去查一次状态,而不是直接 requestPermissionsFromUser。因为受限权限在没有预授权的情况下,动态申请大概率不会成功,先查询可以少走一次无效申请。
关于跳设置页,不同系统版本对设置页的 abilityName 定义可能稍有差异。有些版本是 com.huawei.hmos.settings.MainAbility,有些版本则要求带参数指定权限页。稳妥的方式是统一跳到应用详情页,由用户自行进入“权限管理”再打开目标开关。虽然路径多一步,但至少不会因找不到指定页面而崩溃。
如果你要处理的是多个权限的批量申请,我建议分两类走:
- 普通权限:一次性 requestPermissionsFromUser 批量弹窗。
- 受限权限:单独走设置页引导,不要混在同一个数组里。因为混在一起时,系统可能会因为某一个受限权限而把整组请求都中断,前面的普通权限弹窗也会受影响。
- 补充几个容易被忽略的细节
7.1 白名单和系统版本升级
有朋友问过我,如果应用已经在正式环境用上了受限权限,之后手机系统升级,这个权限会丢吗?
我的实测结论是:大部分情况下不丢。因为白名单授权通常会跟着应用签名走,系统升级时会自动恢复安装时的授权状态。但有个例外,就是你旧版本升级到新版本时,如果新版 module.json5 里新增了一个之前没有申请过的受限权限,那么系统依旧会拦你,不会因为你原来已经有一个受限权限就自动放行新增项。新增权限依然要重新走申请流程。
7.2 企业内部分发场景里的受限权限
如果你的应用并不是走华为应用市场公开上架,而是企业内部分发,走的又是另一套逻辑。企业设备可以通过企业MDM下发的策略配置,把目标应用的签名指纹提前加入系统的受信任 ACL 中。这种情况下,应用无需再去应用市场申请受限权限,但需要有企业设备的管理员权限去统一下发策略。
也就是说,受限权限不一定都得靠“市场审核”这一条路,内网环境下如果你们有设备管理平台,也可以走白名单预置的方式,但要求更高,一般小团队没有这套基建,还是老老实实走公开申请流程吧。
7.3 隐私声明和使用说明别忘同步更新
申请受限权限,本质上已经不只是技术问题了,它涉及用户对你应用能力和数据使用的知情权。如果应用内没有独立的隐私政策页面,或者隐私政策里没提你申请了受限权限、用来干嘛、数据存在哪、是否分享给第三方,审核侧可能会直接驳回。建议在上架前就把隐私政策里关于权限的章节补全,最好能对应上申请材料里写的业务场景。
我把自己的应用里一段常见的隐私政策说明也放上来,给大家做个参照格式:
本应用需要访问设备存储中的公共媒体文件,用于在您主动导入文件时读取所选内容。导入过程在本地完成,文件内容不会上传至服务器。您可以在系统设置中随时关闭本权限。
这段描述不复杂,但明确讲了几个审核员关注的点:主动触发(用户主动导入)、用途(本地读取)、数据流(本地完成,不分享)、撤回方式(设置里可关闭)。
写在后面
受限权限申请这件事,单独看代码实现并不复杂,真正复杂的是它背后的体系逻辑:权限分级、ACL 白名单、签名信任、市场审核、隐私合规。这几块得串起来想,才能一次性把问题想明白。
如果在开发过程中遇到一个问题,我的建议是别急着在代码里瞎试,先静下来查这几个关键信息:当前使用的签名类型是什么;目标权限在文档里的权限等级是什么;该权限在目标系统版本上是否受限;你的应用有没有完成相关权限的申请流程。把这四条核对完,90% 的问题其实已经定位到了。剩余 10% 的问题,多半出在系统版本细节差异和审核材料上面,那就需要拿着日志和文档一步一步细抠了。
