[鸿蒙从零到一] HarmonyOS 权限申请与隐私合规实战
相机、定位、麦克风和通讯录等能力让应用连接真实设备,也会触及用户敏感数据。权限代码能正常弹窗,并不代表产品已经合规:应用还需要说明使用目的、只在必要时申请、处理拒绝结果,并保证用户撤回授权后核心流程不会崩溃。
本文以“拍摄头像”和“读取位置”两个场景为例,梳理 HarmonyOS 应用从权限声明、运行时申请到隐私治理的完整流程。示例使用 ArkTS、Stage 模型和 abilityAccessCtrl。
一、先区分权限类型
HarmonyOS 权限通常可按授权方式理解为两类:
system_grant:系统自动授权,通常用于风险较低的能力;user_grant:必须由用户明确授权,涉及相机、麦克风、位置等敏感能力。
动态申请只针对需要用户授权的权限。开发前应先查阅当前 SDK 对应的权限清单,确认权限名称、授权方式、适用设备和受限条件,不要凭经验拼写权限字符串。
权限设计还应遵循三个原则:
- 最小必要:只申请当前功能真正需要的权限;
- 场景触发:用户操作到相关功能时再申请;
- 可降级:拒绝授权后提供替代路径,而不是让应用无法使用。
例如,更换头像同时支持“拍照”和“从系统选择器选图”。只有用户点击拍照时才需要相机权限,选择已有图片不应被相机权限阻断。
二、在 module.json5 中声明权限
应用使用权限前,需要在模块配置的 requestPermissions 中声明。相机与位置示例如下:
{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:location_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
reason 应引用资源文件,以便多语言适配。文案需要直接说明目的,例如“用于拍摄并设置个人头像”,不要只写“需要相机权限”。usedScene 描述权限在哪个 Ability、什么使用时机生效,配置应与真实业务一致。
不同 API 版本对具体权限和配置可能有调整。精确定位、后台定位等能力通常有更严格的要求,应以工程目标 SDK 的官方文档为准。
三、封装权限检查
abilityAccessCtrl 提供权限管理能力。页面不应到处复制鉴权逻辑,可以先封装一个轻量服务:
import {
abilityAccessCtrl, bundleManager, common } from '@kit.AbilityKit'
import {
BusinessError } from '@kit.BasicServicesKit'
export class PermissionService {
private manager = abilityAccessCtrl.createAtManager()
async check(permission: Permissions): Promise<boolean> {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
)
const tokenId = bundleInfo.appInfo.accessTokenId
const result = await this.manager.checkAccessToken(tokenId, permission)
return result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
async request(
context: common.UIAbilityContext,
permissions: Permissions[]
): Promise<boolean> {
try {
const result = await this.manager.requestPermissionsFromUser(
context,
permissions
)
return result.authResults.every((value: number) => value === 0)
} catch (error) {
const businessError = error as BusinessError
console.error(`request permission failed: ${
businessError.code}`)
return false
}
}
}
checkAccessToken 适合进入功能前快速检查当前授权状态。不要将授权结果永久缓存在内存中,因为用户可能在系统设置中撤回权限。每次执行敏感操作前重新检查,才能反映真实状态。
批量申请多个权限时,返回结果与权限数组一一对应。示例用 every 表示全部权限都通过;实际业务如果允许部分能力可用,应逐项建立结果映射,不要把部分拒绝误判成全部失败。
四、在用户操作时发起申请
页面通过 UIAbility 上下文发起申请。下面实现拍照按钮:
import {
common } from '@kit.AbilityKit'
import {
promptAction } from '@kit.ArkUI'
@Entry
@Component
struct ProfilePage {
private permissionService = new PermissionService()
private getContext(): common.UIAbilityContext {
return getContext(this) as common.UIAbilityContext
}
private async takePhoto(): Promise<void> {
const cameraPermission: Permissions = 'ohos.permission.CAMERA'
let granted = await this.permissionService.check(cameraPermission)
if (!granted) {
granted = await this.permissionService.request(
this.getContext(),
[cameraPermission]
)
}
if (!granted) {
promptAction.showToast({
message: '未获得相机权限,可从相册选择头像' })
return
}
this.openCamera()
}
private openCamera(): void {
// 调用相机能力,并在完成后释放会话等资源
}
build() {
Column({
space: 12 }) {
Button('拍摄头像')
.onClick(() => this.takePhoto())
Button('从相册选择')
.onClick(() => {
// 使用系统选择器选择图片
})
}
.width('100%')
.padding(24)
}
}
这个流程只在用户主动点击“拍摄头像”后触发系统弹窗。用户拒绝时,页面保留系统选择器入口,功能仍然可以完成。
不要在应用启动、首页出现或没有明确上下文时一次性申请所有权限。突兀的弹窗无法让用户理解用途,也会降低授权意愿。
五、处理拒绝和无法再次弹窗
一次拒绝不应立即反复申请。应用应保留当前页面状态,并在用户再次触发功能时,用简短业务文案解释权限用途。
如果系统不再展示授权弹窗,应引导用户前往系统设置管理权限。引导要满足两个条件:
- 用户主动触发了依赖权限的功能;
- 页面清楚说明需要开启哪项权限以及用于什么。
不要使用虚假倒计时、全屏遮挡或连续弹窗迫使用户授权。也不要把隐私授权与账号协议捆绑成一个不可拆分的同意按钮。
在产品状态建模中,可以将结果明确区分:
enum PermissionState {
Granted,
Denied,
Unavailable
}
Denied 表示本次未授权,界面可保留再次触发入口;Unavailable 表示当前环境或策略下无法继续申请,界面提供设置入口或替代能力。具体状态判断接口应结合目标 SDK 的授权返回值实现。
六、位置权限的精度与降级
位置业务尤其需要避免“权限越高越好”。如果天气、附近门店等功能只需大致区域,应优先申请模糊位置;只有导航、运动轨迹等确实依赖精确坐标的场景,才申请精确位置能力。
const permissions: Permissions[] = [
'ohos.permission.APPROXIMATELY_LOCATION'
]
const granted = await permissionService.request(context, permissions)
if (!granted) {
// 允许用户手动选择城市
showCitySelector()
}
手动选择城市是有效的降级方案。它不仅覆盖拒绝授权,还能处理定位关闭、室内信号差、服务异常等情况。
后台持续定位的隐私影响更大。若业务无法证明其必要性,就不应申请;确有必要时,还要提供持续可感知提示、明确关闭入口和合理的数据保留周期。
七、权限之外的数据治理
获得权限只代表用户允许应用调用某项系统能力,不代表应用可以无限制收集、上传和保存数据。完整的隐私合规还包括:
- 收集前明确数据类型、用途和处理方式;
- 只采集完成功能所需的最少字段;
- 传输敏感数据时使用安全连接;
- 本地敏感数据加密保存,日志中避免输出原值;
- 设置合理保留期限,到期删除或匿名化;
- 向用户提供查询、更正、删除和撤回授权入口;
- 第三方 SDK 的采集行为也纳入清单和审查。
以头像为例,完成裁剪和上传后,应及时删除临时文件;日志只记录任务状态和错误码,不记录本地文件绝对路径、访问令牌或可识别个人的信息。
八、建立权限使用清单
项目中可以维护一份权限清单,让产品、开发、测试和合规人员共享同一事实来源:
| 权限 | 触发功能 | 申请时机 | 拒绝后的方案 | 数据去向 |
|---|---|---|---|---|
| 相机 | 拍摄头像 | 点击拍摄按钮 | 系统选择器选图 | 裁剪后上传 |
| 模糊位置 | 附近门店 | 点击定位按钮 | 手动选择城市 | 仅用于当前查询 |
| 麦克风 | 语音输入 | 点击录音按钮 | 文本输入 | 转写后删除音频 |
每次新增权限都需要回答:是否有无权限方案、是否可以降低权限等级、数据保存多久、是否会传给第三方。无法回答时,不应直接把权限加入配置。
九、测试与排查
权限测试不能只覆盖“点击允许”。至少应验证以下路径:
- 首次申请并允许,功能正常执行;
- 首次拒绝,页面展示可理解的降级方案;
- 再次触发功能,申请行为符合系统策略;
- 在系统设置撤回权限后,返回应用不会继续使用旧状态;
- 多权限申请中只有部分通过,业务不会越权调用;
- Ability 切换前后台或销毁时,不持有失效上下文;
- 无网络、定位服务关闭等非权限异常不会被错误提示为“权限不足”;
- 隐私说明、应用内文案与实际采集行为一致。
调试时应记录权限名称、授权结果和业务阶段,但不要记录用户敏感内容。权限已授予却调用失败时,还需要检查设备能力、系统开关、模块配置、API 版本和资源释放,而不是循环申请权限。
十、常见误区
只在配置文件声明,不做动态申请
user_grant 权限仍需要用户在运行时授权。声明只是告诉系统应用可能使用它。
用户拒绝后仍调用受保护接口
所有敏感操作都应以实时检查结果为前置条件。拒绝后直接调用只会产生异常或不可预测体验。
用一个权限阻塞整个应用
权限应与具体功能绑定。地图定位被拒绝,不应影响浏览内容、账号设置等无关能力。
权限说明与真实用途不一致
声明写“改善体验”,实际却用于广告画像,既缺乏透明度,也会带来合规风险。用途变化时,权限文案、隐私政策和数据处理流程都要同步更新。
总结
HarmonyOS 权限开发的核心不是把系统弹窗调出来,而是建立一条可审计、可降级的数据使用链路:在 module.json5 准确声明,在用户触发场景中检查和申请,对拒绝结果提供替代方案,并在获得授权后继续执行最小采集、安全存储和及时删除。
当权限被视为具体功能的临时能力,而不是应用启动时必须拿到的通行证,用户更容易理解授权目的,代码边界也会更清晰。再配合权限清单、撤回入口和完整测试,应用才能同时获得稳定体验与隐私合规能力。