HarmonyOS 7 AbilityKit + AppGallery Connect:权限声明、运行时授权与隐私说明对账机制【鸿蒙心迹】
这篇从一次“代码没问题,为什么上架前还是不放心”的整理开始。Demo 项目叫 ReleaseGuard,目标不是模拟审核平台,而是在提交版本前把三份最容易漂移的信息重新对齐:包里声明了什么权限、代码实际在什么时候申请、AppGallery Connect 和隐私政策里又写了什么。

很多上架问题并不复杂,麻烦的是信息分散。module.json5 在工程里,运行时权限申请散在页面和业务服务里,第三方 SDK 的数据处理说明又可能在另一份文档中。开发到后期一改需求,最常见的情况不是功能坏了,而是“代码已经不用了,声明还在”“声明加了,使用原因没同步”“SDK 换了版本,隐私政策还是旧描述”。
华为当前的应用发布指引明确要求,上架前要检查应用信息与包是否完整、运行是否稳定、隐私合规相关内容是否满足要求;对于检测到敏感隐私权限或受限权限的应用,AppGallery Connect 的发布页面还会要求配置对应隐私说明。部分服务的官方上架说明也明确提醒:集成第三方 SDK 时,需要在隐私政策中逐一说明 SDK 收集个人信息的目的、方式和范围。
我做 ReleaseGuard 的原因,就是不想等到提交之后才把这些信息重新拼起来。
一、我先把“审核前自检”缩成 7 个可回答的问题
ReleaseGuard 首页没有做很多花哨功能,只有一个 PRECHECK 7/7 PASS。这 7 项不是平台官方固定清单,而是我在项目内部定义的工程门槛:
- 当前版本号和待提交包一致;
- 权限声明都能找到业务用途;
- 实际需要用户授权的权限有明确触发入口;
- 不再使用的权限已经从包声明中移除;
- 第三方 SDK 清单与隐私披露一致;
- AppGallery Connect 需要填写的隐私说明已经核对;
- Release Profile、包名和版本信息完成最终对账。
它最重要的价值不是“自动替代审核”,而是让研发团队在提交前有一份能落到代码和包的证据。
图 03 就是这份自检结果。这里最显眼的不是 CAMERA,而是 LOCATION:页面明确显示 NOT DECLARED。因为当前业务已经不需要位置能力,所以正确状态不是“授权关闭”,而是压根不要把不需要的权限继续留在声明里。

这点特别容易混淆。权限治理不是把所有权限都申请一遍然后让用户拒绝,而是业务需要什么就声明什么,真正要访问用户隐私信息或系统敏感能力时,再在合适的业务时机请求授权。
二、声明、授权、实际功能,是三件不同的事
我把权限问题拆成三层以后,很多排查会变得很直接。
第一层是包声明。比如相机权限是否出现在 module.json5 的 requestPermissions 中,使用原因是否写清楚。
第二层是运行时状态。即使声明了 CAMERA,也不代表用户已经授权。应用仍要查询当前状态,并在真正需要拍摄时申请。
第三层是业务触发。ReleaseGuard 的相机权限只在用户点击“扫描证件”后申请,而不是一打开首页就弹窗。
这段代码解决什么问题:让包内权限声明本身就带上明确使用原因,避免后期只看到权限名,不知道是谁加的。
{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:permission_camera_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
在这份 Demo 里,原先测试阶段加过 LOCATION,后来“扫描证件”流程不再依赖位置。我的处理不是在代码里永远不调用它,而是把声明一起删掉,再让自检工具把“未使用权限数量”统计为 0。
项目里最好给每个权限维护一个 owner。不是为了流程复杂化,而是需求下线时能快速知道这条声明应该由谁确认。如果所有权限都没人负责,版本越迭代,历史残留越多。
三、运行时授权不要写成启动仪式,应该贴着用户动作发生
HarmonyOS 的权限接口已经提供了查询自身权限状态和向用户请求授权的能力。工程里真正要决定的是“什么时候调用”。
ReleaseGuard 的逻辑是:用户进入首页不会出现授权弹框;点击“扫描证件”后,先查询 CAMERA 当前状态,如果尚未获得授权,再调用 requestPermissionsFromUser()。
这段代码解决什么问题:把相机授权和“扫描证件”这个明确动作绑定,避免页面启动时无上下文弹权限。
import {
abilityAccessCtrl,
common,
Permissions,
PermissionRequestResult
} from '@kit.AbilityKit';
const CAMERA: Permissions = 'ohos.permission.CAMERA';
async function requestCameraFromScanButton(
context: common.UIAbilityContext
): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const before = atManager.getSelfPermissionStatus(CAMERA);
console.info(`[AUTH] before=${before}`);
const result: PermissionRequestResult =
await atManager.requestPermissionsFromUser(context, [CAMERA]);
const granted = result.authResults.length > 0 && result.authResults[0] === 0;
console.info(`[AUTH] request source=scan_button granted=${granted}`);
return granted;
}
实际开发时,GrantStatus 的判断最好使用对应枚举,而不是像演示代码一样依赖裸数字。这里把判断简化,是为了突出调用链:用户动作 → 查询状态 → 必要时请求 → 根据结果继续业务。
还要注意,用户拒绝并不是异常崩溃。用户不授权 CAMERA 时,ReleaseGuard 会停在“需要相机权限才能扫描”的说明页,同时提供手工录入入口。只有把“拒绝”当成一条正常产品路径,权限申请才不会变成强迫用户通过的闸门。
图 02 对应的就是这条调用链:中间代码标了 requestPermissionsFromUser(),右侧模拟器显示相机已经声明,而 LOCATION 没有声明,底部日志能看到 request source=scan_button 和授权结果。

四、我写了一个轻量扫描器,专门找“声明了但项目里没人用”的权限
只靠人工搜代码很难长期稳定。我后来加了一个构建前脚本,思路很简单:读取 module.json5 的 requestPermissions,再对 src/main/ets 中的权限常量和授权调用做扫描,得到一份“声明集合”和“引用集合”的差集。
它不会百分之百理解业务,也不负责判断某个权限是否法律合规,但特别适合抓两类低级问题:
- 权限已经不用,声明还留着;
- 新代码开始申请某个权限,但包声明没同步。
这段代码解决什么问题:在提交包之前,用脚本提前发现权限声明和代码引用的明显漂移。
import fs from 'node:fs';
import path from 'node:path';
import JSON5 from 'json5';
const modulePath = path.resolve('entry/src/main/module.json5');
const moduleConfig = JSON5.parse(fs.readFileSync(modulePath, 'utf-8'));
const declared = new Set<string>(
(moduleConfig.module.requestPermissions ?? []).map(
(item: { name: string }) => item.name
)
);
const etsRoot = path.resolve('entry/src/main/ets');
const sourceText = collectEtsText(etsRoot);
const referenced = new Set<string>();
for (const permission of declared) {
if (sourceText.includes(permission)) {
referenced.add(permission);
}
}
const unused = [...declared].filter(item => !referenced.has(item));
console.log(`[PREFLIGHT] declared=${declared.size}`);
console.log(`[PREFLIGHT] unusedPermission=${unused.length}`);
console.log(`[PREFLIGHT] unused=${unused.join(',') || 'NONE'}`);
这里的 collectEtsText() 可以自己递归目录实现,也可以放到现有工程脚本里。更重要的是理解它的边界:如果权限名由三方库内部使用、动态拼接、Native 层引用,简单字符串搜索可能误判。所以这个工具输出的是“需要人工复核”,不是“自动删权限”。
我的做法是:脚本发现差异就让 CI 给出 warning;准备 Release 包时,如果仍存在未解释差异,再升级为阻断。这样平时开发不会太重,上架前又有一道明确门槛。
五、第三方 SDK 最容易漏的不是代码,而是说明文档版本
很多项目权限已经收得很干净,最后还是会在第三方 SDK 这里出现信息不一致。
原因很现实:SDK 升级通常发生在依赖文件里,隐私政策却可能由产品、运营或法务维护。研发把 SDK 从 1.x 升到 2.x 后,如果数据处理范围发生变化,隐私政策不一定有人同步改。
所以 ReleaseGuard 的 SDK 对账不扫描“有没有三方库”这么简单,而是维护一份发布清单:
SDK 名称 / 当前版本 / 使用目的 / 涉及数据 / 隐私声明链接 / 本版本是否变化
华为部分服务的上架说明明确提示:集成第三方 SDK 的应用,需要在隐私政策中逐一明示 SDK 收集个人信息的目的、方式和范围。我的工程做法是把这条要求落成一个可检查字段:2 项 SDK,2/2 已披露。
这也解释了为什么图 03 中第三方 SDK 卡片不是“检测到 2 项”就结束,而是继续显示“隐私披露 2/2 完成”。只发现依赖,不确认说明,依然没有完成对账。
六、AppGallery Connect 的隐私说明,不能用“代码里有 reason”替代
这是我最想强调的一点。
module.json5 里的 reason 解决的是包内权限使用原因;AppGallery Connect 发布页面里的隐私说明,是上架流程的一部分;应用自己的隐私政策又是面向用户的公开说明。三者有关联,却不是同一个字段的三个副本。
当前 AppGallery Connect 文档说明,如果应用包被检测到获取敏感隐私权限或使用受限权限,需要在发布流程里配置对应隐私说明;其中受限权限还可能需要额外的使用场景材料。具体哪些字段需要填写,应以当前上传包扫描结果和发布页面提示为准。
所以 ReleaseGuard 不会自作主张地说“有 CAMERA 就一定审核失败”,而是做两件事:
- 告诉你当前包有哪些需要重点关注的权限;
- 记录本版本是否已经完成 AppGallery Connect 对应说明的人工确认。
这种设计虽然不“全自动”,却更可靠。因为上架规则、页面字段和权限分类可能变化,工程工具不应该把平台规则永久硬编码成一个不会更新的 if/else。
七、版本号、包名和 Release Profile,要和权限一起看
有一次我排查权限问题查了很久,最后发现上传的根本不是刚才测试的那个包。开发环境、测试包、Release 包同时存在时,这种低级错误一点也不少见。
因此 ReleaseGuard 最后的对账页会把下面几项固定显示出来:
- 应用版本
1.6.0 (106); - 包名
com.example.releaseguard; - 当前构建类型
Release; - Release Profile 校验
MATCH; - 敏感隐私权限
1 项(CAMERA); - 受限权限
0 项; - 未使用权限
0 项; - 第三方 SDK 信息披露
2/2 MATCH; - 隐私说明
CONFIGURED。
图 04 就是这张“提交前最后看一眼”的页面。红圈标出来的不是为了好看,而是两个最容易出现历史残留的位置:未使用权限和隐私说明状态。

这里的 7/7 PASS 只表示项目内部预检通过,并不代表华为应用市场已经审核通过。文章和工具都应该把这个边界说清楚,否则内部自检很容易被误解成平台审核结论。
八、权限问题最有效的排查方式,是沿着一条证据链往回找
当测试同学说“系统设置里看不到这个权限”,或者审核前发现权限说明对不上,我现在不会先翻 UI 页面,而是按固定顺序检查:
第一步:看包声明。 module.json5 有没有这个权限?reason 和 usedScene 是否仍符合当前业务?
第二步:看运行时调用。 有没有真正调用 requestPermissionsFromUser()?调用入口是什么?用户拒绝后怎么处理?
第三步:看日志。 过滤权限请求日志,确认请求发生在预期操作之后,而不是应用启动就发生。
第四步:看发布配置。 上传的是不是当前 Release 包?版本号、包名、签名配置是否一致?AppGallery Connect 是否扫描到需要填写的隐私说明?
第五步:看隐私政策与 SDK 清单。 实际集成内容和公开说明是否仍是同一个版本。
这条链路最大的好处是不用猜。每一层都有明确证据,问题停在哪一层,就解决哪一层。
九、我更愿意把上架合规当成持续构建问题,而不是发布当天的文档问题
上架审核最难处理的情况,是所有人都等到发布当天才集中检查。那时任何一个字段不一致,都会变成“谁最后改过”的追责题。
ReleaseGuard 最终没有做成一个庞大的审核模拟器,而是变成三块很小的能力:
- 开发阶段:权限和调用差异给 warning;
- Release 构建阶段:版本、权限、SDK 清单生成报告;
- 提交前:人工确认 AppGallery Connect 隐私说明和公开隐私政策。
真正能自动化的就自动化,必须依赖平台当前页面和人工判断的就保留人工确认。这样比假装所有规则都能被脚本准确判断更稳。
如果后续项目继续变大,我还会把相机、定位、通讯录等权限按业务模块归属,把三方 SDK 版本变更加入 Pull Request 模板,再把 ReleaseGuard 的报告作为发布附件保存。这样半年后回头看某个版本,也能知道当时为什么申请某个权限、由哪个功能触发、隐私说明是否同步。
十、我专门保留了一组“拒绝授权”测试,不让 PASS 只建立在顺利路径上
权限功能最容易出现一种假稳定:开发者自己的设备已经长期授权,于是每次测试都从 GRANTED 开始。代码看起来非常顺,直到新用户第一次安装,或者用户在系统设置里撤回权限,真正的问题才出现。
ReleaseGuard 因此固定跑三种状态:未决定、已授权、已拒绝。未决定状态下点击“扫描证件”,应该出现系统授权流程;已授权状态下再次点击,不应重复制造无意义请求;已拒绝状态下,页面应该解释为什么功能受限,并给出可继续使用的替代路径,而不是无限重复弹窗。
我把这三种情况写进测试记录:
NOT_DETERMINED -> 用户点击扫描 -> 请求 CAMERA -> 依据用户选择继续
GRANTED -> 用户点击扫描 -> 直接进入拍摄流程
DENIED -> 用户点击扫描 -> 展示说明 / 可用替代方式 / 必要时引导设置
这里尤其要避免“用户拒绝一次就马上再弹一次”。从产品体验看,这会让授权变成强制拦截;从排查角度看,也会让日志里充满重复请求,反而难以判断真正的触发来源。
测试时我还会主动从系统设置里撤回 CAMERA,再回到应用继续操作。这个动作能验证两个细节:应用是否在每次关键操作前重新确认状态,以及 UI 是否会把之前的“已授权”缓存成永久状态。权限属于可变状态,不能只在应用首次启动时读一次。
1. “设置页里能看到权限”也不是最终验收目标
官方权限 FAQ 提到,不同 API 阶段系统设置页面对权限展示行为存在差异,排查时应该回到 module.json5 声明和 requestPermissionsFromUser() 的真实调用。对项目来说,更可靠的验收不是“设置页出现某个开关”,而是“功能触发、系统授权状态、业务降级逻辑三者一致”。
因此 ReleaseGuard 的报告不会简单把“系统设置有开关”记成 PASS,而是保留请求来源 scan_button,让测试人员知道这次授权是在哪个动作里产生的。
十一、CI 报告最好能被人读懂,而不是只返回一个 0 或 1
做成脚本以后,我一开始只让命令返回成功或失败。很快就发现这对排查不够友好。CI 红了以后,开发者还得重新下载日志,自己猜是哪一项没过。
后来输出改成结构化摘要:
[PACKAGE] version=1.6.0(106) bundle=com.example.releaseguard
[PERMISSION] declared=1 referenced=1 unused=0
[SDK] detected=2 disclosed=2
[PRIVACY] appgallery=confirmed policy=confirmed
[PROFILE] releaseProfile=MATCH
[PREFLIGHT] PASS 7/7
如果失败,则必须写具体原因,例如:
[PREFLIGHT] FAIL
- unused permission: ohos.permission.LOCATION
- sdk disclosure missing: analytics-sdk 2.4.1
这种输出还有一个额外价值:它很适合作为 Release 构建产物的一部分归档。将来某个版本出现争议,不用依赖谁的记忆,直接看当时提交前生成的报告就知道包里是什么状态。
当然,这份报告仍然不能替代平台审核。它只是把团队能控制的工程信息做成可重复检查,让“提交前我应该看什么”从个人经验变成项目流程。
十二、隐私政策不是静态附件,最容易在版本迭代里落后半拍
我以前把隐私政策理解成“上线前准备一次”,后来发现这几乎注定会过期。真正的应用会不断加登录方式、统计 SDK、地图、支付、相机、文件选择等能力,每一次依赖或业务变化都可能让原来的说明失去准确性。
所以我把隐私政策当成版本配置的一部分。每次 SDK 版本、权限声明或数据处理流程变化时,都要求 PR 描述里回答一个问题:这次变化是否需要同步更新隐私说明?
如果答案是“否”,也要说明为什么。这样做会多几十秒,却能避免几个月后没人知道某条隐私描述对应的是哪个历史实现。
对于系统 Picker 这类能够减少额外权限申请的能力,也应该在设计阶段优先考虑。能通过系统提供的受控选择流程完成目标,就没有必要为了“代码方便”扩大长期权限范围。权限越少,业务边界越清晰,后续测试和上架材料也更容易维护。
十三、最后一次人工复核,我只看四种“不一致”
自动化脚本跑完后,我仍然会留一个人工确认步骤,但不会让大家重新通读所有文件,而是专门找四种不一致:
- 代码与声明不一致:代码要用,包没声明;或者包声明了,代码已经不用;
- 声明与触发不一致:权限用途写的是扫描证件,实际却在首页启动就申请;
- 依赖与隐私政策不一致:SDK 已升级或新增,公开说明仍是旧版本;
- 测试包与提交包不一致:开发机验证的是 Debug 包,真正上传的是另一个 Release 构建。
这四类问题一旦排除,剩下的审核工作就更接近平台规则本身,而不是项目内部信息漂移造成的返工。
十四、这套对账机制解决的不是“怎么过审”,而是“怎么让提交材料和真实应用保持一致”
应用审核不是一个可以靠技巧绕过的流程。真正能降低返工的,是让提交信息准确反映当前版本的真实行为。
这篇里我反复强调“对账”,原因就在这里:
声明要和代码对上,代码要和用户动作对上,用户动作要和隐私说明对上,隐私说明又要和当前上传包对上。
ReleaseGuard 的价值不是替开发者判断平台会不会通过,而是在提交前把明显的不一致暴露出来。对一个长期迭代的 HarmonyOS 项目来说,这比临发布时翻几十个文件、逐项回忆“这个权限还有没有用”靠谱得多。
参考资料
- 华为开发者:提交 HarmonyOS 应用与鸿蒙 APP
- 华为开发者:上架审核相关社区话题
- 华为开发者:AppGallery Review Guidelines / Configuring a Privacy Description
- 华为开发者:应用权限管理 FAQ(getSelfPermissionStatus、requestPermissionsFromUser)
- 华为开发者服务上架说明:第三方 SDK 隐私披露要求
更多推荐



所有评论(0)