HarmonyOS 权限申请避坑:声明、弹窗、审核说明一次讲清

请添加图片描述

应用上架审核里,权限问题很常见:功能里确实需要定位,但 module.json5 没声明清楚;用户点击“附近门店”才需要定位,应用却一启动就弹权限;相册权限只是上传头像,却在审核说明里写成“提升体验”。这些问题不一定是代码跑不通,而是权限申请链路不完整。

这篇文章只讲一件事:HarmonyOS 应用申请权限时,如何把业务场景、配置声明、运行时弹窗、拒绝处理和 AGC 审核说明串起来。

一、资料定位:权限不是只写一行配置

类型 资料/能力 本文使用点
官方权限文档 访问控制开发概述 权限类型、授权边界
官方申请权限 向用户申请授权 运行时申请、授权结果处理
官方配置 module.json5 配置文件 权限声明位置
官方上架 AppGallery Connect 提交审核 审核资料、版本提交

本文以 HarmonyOS 5.0.0+、Stage 模型、ArkTS 为边界。示例围绕“附近路线推荐”功能:只有用户点击附近路线时才申请定位权限。

二、合规流程:先有场景,再申请权限

请添加图片描述

权限申请按六步走:

  1. 确认业务场景。
  2. 在配置里声明权限。
  3. 给用户说明使用目的。
  4. 在用户触发功能时运行时申请。
  5. 处理拒绝和永久拒绝。
  6. 上架前复查权限与功能是否一致。

不要把权限申请写成“进入首页就弹”。这会让用户反感,也容易被审核认为申请时机不合理。

三、权限合规架构

请添加图片描述

推荐目录:

entry/src/main/ets/
├── pages/nearby/
│   └── NearbyRoutePage.ets
├── permission/
│   ├── PermissionReason.ets
│   ├── PermissionGuard.ets
│   └── DeniedHandler.ets
└── service/location/
    └── NearbyLocationService.ets
文件 职责
module.json5 声明应用会使用哪些权限
PermissionReason 管理给用户看的权限说明
PermissionGuard 执行检查和运行时申请
DeniedHandler 处理拒绝后的降级路径
NearbyLocationService 真正使用定位能力

权限逻辑不要散落在每个按钮里。统一封装后,上架前检查也更容易。

四、先做权限清单:能不用就不用,能按需就按需

功能 权限 申请时机 拒绝后策略
附近路线推荐 位置权限 点击“查看附近” 手动选择城市
扫码导入路线 相机权限 点击“扫码” 输入路线码
上传头像 相册/媒体权限 点击“更换头像” 使用默认头像
语音备注 麦克风权限 点击“录音” 文本输入
网络请求 网络访问 应用使用期间 提示网络不可用

这张表最好在开发前就写。它能避免两个问题:申请了不用的权限,或者用了权限却没说明业务场景。

五、module.json5:声明要准确,不要贪多

下面示例只声明“附近路线”需要的定位权限。具体权限名要以项目目标 API 和官方权限列表为准。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:permission_location_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      }
    ]
  }
}

代码解释:

  1. name 必须和实际能力匹配,不要把暂时不用的权限提前声明。
  2. reason 要写给用户能理解的业务原因,不要写“应用需要权限”。
  3. usedScene 要和实际使用 Ability 对应。
  4. when 表示使用场景,具体取值以官方文档和权限类型要求为准。

权限声明不是越全越保险。声明越多,审核越会关注你是否真的需要。

六、权限说明:用户要知道为什么此刻申请

export interface PermissionReason {
  permission: string;
  title: string;
  message: string;
  fallback: string;
}

export const LOCATION_REASON: PermissionReason = {
  permission: 'ohos.permission.LOCATION',
  title: '需要定位权限',
  message: '用于根据当前位置推荐附近路线,不会在后台持续获取位置。',
  fallback: '如果拒绝授权,可以手动选择城市继续使用路线搜索。'
};

代码解释:

  1. message 要讲业务场景,不讲空话。
  2. fallback 告诉用户拒绝后还能怎么用。
  3. 不要承诺做不到的事,比如“绝不收集任何信息”。
  4. 权限说明和隐私政策里的描述要一致。

如果用户点击“附近路线”,此时弹出定位说明是合理的;如果刚打开应用就弹,用户很难理解。

七、运行时申请:用户触发功能后再弹

import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common';

export class PermissionGuard {
  private atManager = abilityAccessCtrl.createAtManager();

  async ensurePermission(
    context: common.UIAbilityContext,
    permission: Permissions
  ): Promise<boolean> {
    const grantStatus = await this.atManager.checkAccessToken(
      context.applicationInfo.accessTokenId,
      permission
    );

    if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      return true;
    }

    const result = await this.atManager.requestPermissionsFromUser(context, [permission]);
    return result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  }
}

代码解释:

  1. 先检查授权状态,已经授权就不重复弹窗。
  2. requestPermissionsFromUser() 要在用户触发相关功能后调用。
  3. 返回值只表达是否可继续执行,不在这里写业务降级。
  4. 权限类型使用 Permissions,减少字符串写错风险。

真实项目里要根据 API 版本确认导入路径和返回结构。权限代码最怕复制旧版本示例后不复测。

八、页面调用:拒绝后也要有路可走

@Entry
@Component
struct NearbyRoutePage {
  private permissionGuard: PermissionGuard = new PermissionGuard();
  @State private message: string = '点击按钮查看附近路线';

  build() {
    Column({ space: 16 }) {
      Text(this.message)
        .fontSize(18)

      Button('查看附近路线')
        .onClick(() => {
          this.openNearbyRoutes();
        })

      Button('手动选择城市')
        .onClick(() => {
          this.message = '已切换为手动城市选择模式';
        })
    }
    .padding(24)
  }

  private async openNearbyRoutes(): Promise<void> {
    const context = getContext(this) as common.UIAbilityContext;
    const granted = await this.permissionGuard.ensurePermission(
      context,
      'ohos.permission.LOCATION'
    );

    if (!granted) {
      this.message = '未授权定位,可手动选择城市继续使用';
      return;
    }

    this.message = '正在获取附近路线...';
  }
}

代码解释:

  1. 权限申请发生在用户点击“查看附近路线”之后。
  2. 拒绝后不阻断整个页面,而是提供“手动选择城市”。
  3. 页面只关心业务结果,不关心权限底层细节。
  4. 审核时能看出权限和功能存在明确对应关系。

权限合规的一个核心点是“克制”。不是所有功能都必须拿权限,也不是拒绝后就让用户寸步难行。

九、拒绝处理:不要循环弹窗

export class DeniedHandler {
  static buildLocationDeniedTips(): string {
    return '定位权限未开启,附近路线将无法自动推荐。你可以手动选择城市继续使用。';
  }

  static shouldShowGuide(rejectCount: number): boolean {
    return rejectCount <= 1;
  }
}

代码解释:

  1. 拒绝后给出功能影响和替代方案。
  2. 不要循环弹权限弹窗,会严重影响体验。
  3. rejectCount 可以存在本地,用于控制引导频率。
  4. 永久拒绝场景可以引导用户去系统设置,但不要强迫。

很多审核问题不是权限本身,而是申请方式太激进。用户拒绝一次后,应该尊重这个结果。

十、AGC 审核说明怎么写

权限审核说明建议写成“功能 + 时机 + 用途 + 拒绝后影响”。

权限 推荐说明
定位 用户点击“查看附近路线”时申请,用于推荐当前位置附近路线;拒绝后可手动选择城市
相机 用户点击“扫码导入路线”时申请,用于扫描路线二维码;拒绝后可手动输入路线码
相册 用户点击“更换头像/上传图片”时申请,用于选择本地图片;拒绝后使用默认图片
麦克风 用户点击“语音备注”时申请,用于录制路线备注;拒绝后可使用文字输入

不要写这种说明:

为了提升用户体验,需要申请定位权限。

这句话太空,审核人员看不出权限和功能的对应关系。

十一、验证流程

1. 首次安装应用,进入首页,确认不会立即弹定位权限。
2. 点击“查看附近路线”,确认先出现合理说明或系统授权弹窗。
3. 点击拒绝,确认页面提供手动选择城市方案。
4. 再次点击功能,确认不会无限骚扰式弹窗。
5. 授权后进入功能,确认定位能力只在对应功能里使用。
6. 检查隐私政策和 AGC 审核说明是否一致。

建议保留测试截图:

截图 用途
权限触发入口 证明不是启动即申请
系统授权弹窗 证明权限名和时机
拒绝后的降级页面 证明不强迫授权
AGC 权限说明 证明审核材料完整

十二、常见问题排查

现象 可能原因 检查方法 修复建议
审核说权限用途不清 说明太泛 查看 AGC 权限说明 写清功能、时机、用途
用户一进应用就被弹窗 申请时机过早 搜索启动生命周期 移到用户触发后
拒绝后功能崩溃 没有降级路径 拒绝权限测试 提供手动输入或替代能力
配置了权限但申请失败 权限名或 usedScene 不匹配 检查 module.json5 对照官方权限文档
隐私政策和弹窗不一致 多处文案各写各的 对比文案 统一 PermissionReason

十三、上架前验收清单

检查项 是否完成
每个权限都有明确业务场景
没有声明暂时不用的敏感权限
权限申请发生在用户触发功能后
拒绝权限后有可用降级方案
权限说明、隐私政策、AGC 审核说明一致
首次安装、拒绝、授权、系统设置关闭都测试过
日志和埋点不记录敏感权限数据

权限类问题建议在提交审核前单独做一轮“干净安装测试”。不要用已经授权过的开发机直接判断流程是否合规,因为系统可能不会再次弹窗,很多首次授权问题会被掩盖。测试时至少覆盖首次安装、拒绝一次、拒绝后再次点击、系统设置关闭权限、重新授权这几条路径。

审核材料也要和代码同步更新。只要新增、删除或调整了敏感权限,AGC 的权限说明、应用内弹窗说明、隐私政策都要一起检查。三处只要有一处说法不一致,就可能被认为用途不清或告知不充分。

十四、总结

HarmonyOS 权限申请能否顺利通过审核,不只看代码能不能调通,更看整条链路是否合理:

  1. 业务确实需要。
  2. 配置声明准确。
  3. 用户触发后再申请。
  4. 拒绝后有替代方案。
  5. 审核说明写清楚。

权限不是越多越强,而是越克制越稳。把“为什么要申请、什么时候申请、拒绝怎么办”讲清楚,上架风险会小很多。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐