权限申请机制
引言
发布页能定位、能读剪贴板、能跨设备传文件,背后全靠权限支撑。HarmonyOS 的权限体系分为两类:
- system_grant(系统授权):安装时静默授予,用户无感知,如访问网络;
- user_grant(用户授权):运行时弹窗询问,用户可拒绝,如精确定位、读相册。
开发者需要做两件事:一是在module.json5里声明权限(含授权理由、使用场景),二是在代码里请求user_grant 权限(requestPermissionsFromUser)。两件事缺一不可——只声明不请求,用户永远见不到授权弹窗;只请求不声明,直接报错。
本篇以发布页为例,把"声明层 + 请求层 + 消费层"三层权限机制讲透:CommonConstants里的权限数组、BottomToolbar.requestPermissions的运行时请求、以及module.json5中 6 个权限的完整声明。
知识点讲解
abilityAccessCtrl 与 AtManager
abilityAccessCtrl(@kit.AbilityKit)是权限管理模块,核心入口是abilityAccessCtrl.createAtManager()返回的 AtManager(应用权限管理器)。最常用的方法是:
atManager.requestPermissionsFromUser(context,permissions)context:UIAbilityContext,用于定位发起弹窗的页面上下文;permissions:Array<Permissions>,要申请的权限名列表。
返回 Promise,授权完成(无论同意还是拒绝)后 resolve,可进一步用requestPermissionsFromUser的返回值检查授权结果。注意:该方法只能申请 user_grant 权限,且这些权限必须已在module.json5的requestPermissions里声明。
module.json5 权限声明
module.json5的requestPermissions数组每项包含:
name:权限名,如ohos.permission.LOCATION;reason:申请理由(资源引用$string:xxx),授权弹窗中展示给用户看;usedScene:使用场景,abilities指明在哪个 Ability 使用,when是inuse(前台使用)或always(前后台均使用)。
系统对 user_grant 权限强制要求填写reason与usedScene,否则审核或安装阶段可能被拒。
定位权限的分级与联动
HarmonyOS 的定位权限比较特殊,分三级:
ohos.permission.APPROXIMATELY_LOCATION:模糊定位(精度约百米级);ohos.permission.LOCATION:精确定位;ohos.permission.LOCATION_IN_BACKGROUND:后台定位。
系统要求:申请LOCATION必须先申请APPROXIMATELY_LOCATION(捆绑授权);LOCATION_IN_BACKGROUND只在前台两个权限都授予后才可单独申请,且usedScene.when需为inuse。本项目在module.json5中三个都声明了,但运行时只弹窗申请前两个——后台定位权限在实际使用后台场景时才需申请(示例工程未触发)。
剪贴板与媒体库权限的"双轨"
ohos.permission.READ_PASTEBOARD(读剪贴板)和ohos.permission.READ_IMAGEVIDEO(读公共目录图片视频)也是 user_grant 权限。但工程代码巧妙地"绕开"了它们:图库选择走系统PhotoViewPicker(免权限)、粘贴走PasteButton安全控件(系统代读剪贴板)。权限仍然声明在module.json5里,属于"能力声明 + 备而不用"——保证将来用代码级接口(如getPasteDataTest直接读剪贴板)时不缺权限。
结合本项目源码分析
权限数组:CommonConstants.REQUEST_PERMISSIONS
文件路径:entry/src/main/ets/constants/CommonConstants.ets。
import{Permissions}from'@kit.AbilityKit';/* * The request permission. */staticreadonlyREQUEST_PERMISSIONS:Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION',// 模糊定位'ohos.permission.LOCATION'// 精确定位];把权限名收敛到常量类,类型用Permissions联合类型约束,编译期就能发现拼写错误;所有申请权限的地方引用CommonConstants.REQUEST_PERMISSIONS,改权限只需动一处。
运行时请求:BottomToolbar.requestPermissions
文件路径:entry/src/main/ets/view/contentEditor/BottomToolbar.ets。
requestPermissions():void{// 创建应用权限管理器letatManager = abilityAccessCtrl.createAtManager();try{// 弹窗请求定位权限;授权成功后开启定位监听atManager.requestPermissionsFromUser(this.context,CommonConstants.REQUEST_PERMISSIONS).then(() =>{LocationUtil.geolocationOn(this.getGeolocationOn);// 成功(含拒绝)都走到这里}).catch((err: BusinessError) =>{ hilog.info(DOMAIN,TAG,FORMAT,`RequestPermissionsFromUser failed. Cause code:${err.code}, message:${err.message}`); }); }catch(err) { hilog.error(DOMAIN,TAG,FORMAT,'requestPermissionsFromUser err'+JSON.stringify(err)); } }调用时机在aboutToAppear:
aboutToAppear():void{this.requestPermissions(); }这里的this.context是BottomToolbar在组件声明处通过this.getUIContext().getHostContext()拿到的 UIAbilityContext——同一个 context 贯穿了本模块的权限申请、图库选择(getPhotoAccessHelper(context))与分布式文件写入(writeDistributedFile(context, ...)),是各类系统服务共用的"身份凭证",声明一次、全组件复用。
三点值得注意:
- 时机前置:进入发布页立刻申请,授权弹窗尽早出现,用户还没开始操作就有心理预期;拒绝后应用仍能正常编辑内容,只是位置功能不可用——这是"权限失败要优雅降级"的示范。
- Promise 语义:
.then()在用户做出选择后触发,无论同意还是拒绝都会执行。示例工程未校验授权结果就开启定位——geolocationOn内部有 try/catch 兜底,即便未授权,监听注册失败也只记日志,不会崩溃。真实产品应检查返回值AuthResult判断授权结果再决定后续流程。 - 双重异常保护:外层 try/catch 捕获同步异常(如 context 无效),
.catch捕获异步异常,错误都进 hilog 便于排查。
声明层:module.json5 的 6 个权限
文件路径:entry/src/main/module.json5。
"requestPermissions": [ {"name":"ohos.permission.APPROXIMATELY_LOCATION",//模糊定位"reason":"$string:permission_approximately_location",//弹窗展示的申请理由"usedScene": {"abilities": ["EntryAbility"],"when":"inuse"//前台使用时 } }, {"name":"ohos.permission.LOCATION",//精确定位"reason":"$string:permission_location","usedScene": {"abilities": ["EntryAbility"],"when":"inuse"} }, {"name":"ohos.permission.LOCATION_IN_BACKGROUND",//后台定位"reason":"$string:permission_location_in_background","usedScene": {"abilities": ["EntryAbility"],"when":"inuse"} }, {"name":"ohos.permission.READ_PASTEBOARD",//读剪贴板"reason":"$string:permission_read_pasteboard","usedScene": {"abilities": ["EntryAbility"],"when":"inuse"} }, {"name":"ohos.permission.READ_IMAGEVIDEO",//读公共目录图片视频"reason":"$string:permission_read_imagevideo","usedScene": {"abilities": ["EntryAbility"],"when":"inuse"} }, {"name":"ohos.permission.DISTRIBUTED_DATASYNC",//分布式数据同步"reason":"$string:permission_distributed_datasync","usedScene": {"abilities": ["EntryAbility"],"when":"inuse"} } ]6 个权限与理由资源的对应关系(resources/base/element/string.json中均有文案):
| 权限 | 类型 | 用途 | 理由文案要点 |
|---|---|---|---|
| APPROXIMATELY_LOCATION | user_grant | 模糊定位 | "obtain fuzzy device location information" |
| LOCATION | user_grant | 精确定位 | "obtain device location information" |
| LOCATION_IN_BACKGROUND | user_grant | 后台定位 | "obtain device location information while running in the background" |
| READ_PASTEBOARD | user_grant | 读剪贴板 | "read the clipboard" |
| READ_IMAGEVIDEO | user_grant | 读公共图片视频 | "reading of image or video files in the user's public directory" |
| DISTRIBUTED_DATASYNC | user_grant | 跨设备数据同步 | "allow data exchange between different devices" |
DISTRIBUTED_DATASYNC是分布式能力的"门禁":接续(Continue)、分布式文件同步都依赖它,未授权时跨设备功能不可用。它的授权同样走requestPermissionsFromUser运行时弹窗(本项目在主流程中未主动申请,由系统在需要时提示,属示例简化处理)。
理由文案:从声明到弹窗
reason字段指向的资源文案,最终会原样展示在授权弹窗里,是用户判断"要不要给权限"的重要依据。项目在resources/base/element/string.json中为每个权限准备了独立的理由文案,例如定位权限:
{"name":"permission_location","value":"When loading a list of location information, Allows applications to obtain device location information"}文案采用"场景 + 能力"的结构:先说什么时候用(加载位置信息列表时),再说用到什么(获取设备位置信息),让用户对权限用途一目了然。同时zh_CN与en_US目录下各有对应的翻译版本,系统按应用语言自动切换——权限理由的多语言维护,靠的是资源文件的目录约定,代码里零逻辑,这也是工程把reason写成$string:xxx引用而不是硬编码字符串的原因。
这里还藏着一个"声明与使用对应"的细节:usedScene.abilities填的是EntryAbility,when是inuse。inuse表示"应用在前台运行时使用",这是定位、剪贴板这类敏感权限的推荐配置;若某个权限确实需要后台使用(如后台持续定位),才考虑always并配合LOCATION_IN_BACKGROUND单独申请。项目把六项权限的when统一设为inuse,与"仅在发布页使用"的实际情况一致,也符合最小授权原则——声明的内容与代码的实际使用范围一一对应,审计时一目了然。
三层机制如何协同
把发布页的权限体系串成一张图:
声明层module.json5 requestPermissions(6个权限 + reason + usedScene) ↓ 安装时校验(user_grant 必须带 reason/usedScene) 请求层 CommonConstants.REQUEST_PERMISSIONS(运行时申请的精确定位+模糊定位) ↓ requestPermissionsFromUser 弹窗 用户层 同意 → geolocationOn 开启定位 → 位置功能可用 拒绝 → 仅 hilog 记录 → 编辑功能照常,位置列表保留默认示例声明 ≠ 请求:6 个权限声明了,但运行时只主动申请 2 个;其余权限要么走系统选择器/安全控件规避(图库、剪贴板),要么在功能真正触发时由系统引导授权(分布式)。这是"最小权限原则"的实践:能绕开的敏感权限就绕开,绕不开的才申请。
小结
权限申请机制是发布页一切"敏感能力"的起点,本项目的做法可以总结为四句话:
- 声明在 module.json5:6 个权限全部声明,user_grant 权限补齐
reason与usedScene,理由文案走资源文件便于多语言; - 请求在代码层:
abilityAccessCtrl.createAtManager().requestPermissionsFromUser()运行时弹窗,授权成功即开启定位监听; - 常量统一管理:
CommonConstants.REQUEST_PERMISSIONS集中定义权限数组,一处改动全局生效; - 能规避就规避:图库选图走
PhotoViewPicker、粘贴走PasteButton安全控件,把敏感权限的使用面压到最小。
至此,模块三"内容发布模块"的 9 篇文章全部完成。从发布页骨架(14)、焦点与键盘(15),到本地图库(16)、跨设备拉取(17)、AddMedia 详解(18)、文本编辑(19)、底部工具栏(20)、定位与逆地理编码(21)、权限机制(22),一条完整的发布链路已经被逐层拆解。下一篇开始,我们将进入模块四,探索"分布式数据与文件"——看看发布的内容是如何跨越设备无缝接续的。
(本文引用源码:entry/src/main/module.json5、entry/src/main/ets/constants/CommonConstants.ets、entry/src/main/ets/view/contentEditor/BottomToolbar.ets、entry/src/main/ets/utils/LocationUtil.ets)