某个功能下线后,工程里的调用代码删干净了,module.json5 却还留着权限声明。它未必引起编译错误,功能回归时也很容易漏掉。真正到发布前才逐行翻配置,开发者需要同时回答三个问题:权限现在还需要吗、用户授权类权限的说明是否完整、这份声明和当前版本的功能清单是否一致。

这篇不写“自动通过应用市场审核”的万能脚本,而是做一个可重复执行的静态预检。Demo 名为 PermitLens,扫描任务 PL-042。示例工程声明 3 项权限,产品批准的基线是 2 项,差异结果是“多余权限 1 项、说明字段缺失 1 项”,整体状态“需复核”。这是设计好的测试输入和预期输出,不是某个真实应用的发布结论。

一、审核之前,先分清系统规则和团队基线

华为官方的《在配置文件中声明权限》说明,权限声明应写在模块的 module.json5 中,通过 requestPermissions 配置;对 user_grant 或 manual_settings 类型,reason 和 usedScene 具有特定要求。usedScene 包括实际使用的 Ability 和 when,而 when 可以使用 inuse 或 always。权限类型不同,不能简单规定“所有权限都必须填写一套字段”。

团队基线则是另一回事。假设一个图片记录应用,本版本允许网络访问和拍摄照片,批准清单写了 ohos.permission.INTERNET 与 ohos.permission.CAMERA。如果某个历史语音功能仍把 ohos.permission.MICROPHONE 留在配置里,脚本就应该把它标出来。但“超出基线”不等同于“非法权限”;也可能是产品新功能已经上线而清单尚未更新,需要产品、开发与合规人员重新核对。

PermitLens 的输出因此不使用“审核失败”“违规”等定性词,只输出需复核。同样,静态脚本不能仅凭字符串推断应用运行时到底有没有调起权限、弹窗文案是否正确、个人信息实际怎样处理,更不能代替真实上架审核。

二、构造一个会触发预检的配置样本

要让差异扫描有价值,先明确扫描输入。下面是 entry/src/main/module.json5 的简化配置片段:为了突出问题,省略了 Ability、包名等其他工程字段。它故意保留一个缺少用户授权说明的麦克风权限,属于负向演示,不应照搬进生产包。

{
  module: {
    requestPermissions: [
      { name: 'ohos.permission.INTERNET' },
      {
        name: 'ohos.permission.CAMERA',
        reason: '$string:camera_reason',
        usedScene: { abilities: ['EntryAbility'], when: 'inuse' }
      },
      { name: 'ohos.permission.MICROPHONE' } // 故意遗漏说明的样本
    ]
  }
}

这里最重要的不是第三行有没有逗号,而是给规则设了边界:INTERNET 不是要按 CAMERA 的用户授权方式机械检查,CAMERA 则需要校验原因资源和使用场景,MICROPHONE 在这份测试样本里既不在团队基线内,又缺少必要说明。脚本只能检查配置结构,无法知道 $string:camera_reason 在所有语言包中是否都写得恰当;这部分留给后续资源扫描与人工复核。

我为演示固定了一份独立的 tools/baseline/permissions.json,内容只有两个允许项:["ohos.permission.INTERNET", "ohos.permission.CAMERA"]。这并不是 HarmonyOS 的官方允许列表,而是本项目当前版本的人工审批结果。不要把它发布成所有应用适用的通用白名单。

三、Node.js 在构建前扫出两种风险

JSON5 配置不能无脑交给 JSON.parse。示例脚本 tools/audit-permissions.mjs 使用社区 json5 包解析(需在脚本运行环境安装),再从 module.requestPermissions 读权限数组;扫描结果只和本地基线比较,输出可用于 CI 的摘要。

// tools/audit-permissions.mjs(项目根目录执行)
import { readFileSync } from 'node:fs';
import JSON5 from 'json5';

const file = 'entry/src/main/module.json5';
const config = JSON5.parse(readFileSync(file, 'utf8'));
const declared = config.module?.requestPermissions ?? [];
const baseline = new Set([
  'ohos.permission.INTERNET', 'ohos.permission.CAMERA'
]);
const userGrantInThisDemo = new Set([
  'ohos.permission.CAMERA', 'ohos.permission.MICROPHONE'
]);

const extra = declared.filter(p => !baseline.has(p.name));
const missing = declared.filter(p =>
  userGrantInThisDemo.has(p.name) &&
  (!p.reason || !p.usedScene ||
   !Array.isArray(p.usedScene.abilities) ||
   p.usedScene.abilities.length === 0 ||
   !['inuse', 'always'].includes(p.usedScene.when))
);
console.info(`[PermitLens] scan=PL-042 declared=${declared.length} baseline=${baseline.size}`);
console.info(`[PermitLens] extra=${extra.length} missingFields=${missing.length}`);
console.info(`[PermitLens] result=${extra.length || missing.length ? 'REVIEW_REQUIRED' : 'LOCAL_CHECK_OK'}`);
if (extra.length || missing.length) process.exitCode = 1;

这段代码只有两条规则,刻意不写一个虚假的“全量权限分类库”。userGrantInThisDemo 只覆盖该样本涉及的两个用户授权权限,并不代表列出了系统所有 user_grant 或 manual_settings 权限。真实项目要根据目标 SDK 最新官方权限表维护分类,补充重复声明、敏感权限用途、字符串资源存在性和多 HAP 差异等检查。

脚本中的 extra 和 missing 是两种发现类别,不是两组互斥的权限。一个 MICROPHONE 会同时出现在两者中,所以 1 + 1 不能误写成“发现两个不同权限有问题”。摘要中保持“声明总数 3、基线数量 2、多余权限 1、说明字段缺失 1”,让开发者一眼看到问题的来源,而不是被一个无上下文的风险总分误导。

DevEco 图是界面结构演示:左侧工程树有 module.json5、tools/audit-permissions.mjs、baseline/permissions.json,中间展示扫描逻辑,底部 HiLog 则模拟便于检查的摘要与 REVIEW_REQUIRED。图中手机画面同样是展示预检结果的产品原型,并不意味着 Node.js 脚本会自动把输出传入真机。真正接入 CI 时,应将脚本的标准输出保存为构建产物;实际 App 若要展示预检数据,还需要独立的安全导入流程。

四、基线变化不能偷偷放宽检查

为了让脚本能在团队里用下去,还需要把“多余声明”与“基线更新”分开。某个同事给基线直接新增 MICROPHONE,可以让第一条检查消失,但不会让缺失的 reason 和 usedScene 自动变正确。更不能因为 CI 变绿就假定获得了用户授权。

下面是一个可以加入构建脚本的输出收口函数。它不增加新的系统 API,只把差异信息写成普通 JSON 文件,便于后续提交审核单或在制品仓库里追溯。本演示为便于理解,把扫描时间放在展示页文案中;真实脚本可以读取 CI 的构建时间,而不是硬编码为固定日期。

// 与上一段脚本接在一起使用
import { mkdirSync, writeFileSync } from 'node:fs';
const summary = {
  scanId: 'PL-042', declared: declared.length,
  baseline: baseline.size,
  extra: extra.map(p => p.name),
  missingFields: missing.map(p => p.name),
  result: extra.length || missing.length ? 'REVIEW_REQUIRED' : 'LOCAL_CHECK_OK'
};
mkdirSync('build/permit-lens', { recursive: true });
writeFileSync('build/permit-lens/report.json',
  JSON.stringify(summary, null, 2), 'utf8');

运行前在执行环境安装 JSON5 包,例如 npm install --save-dev json5,随后使用 node tools/audit-permissions.mjs。该演示脚本若发现差异会把退出码设置为 1,适合作为“阻止继续自动发布、等待人工复核”的工程门槛;它本身不会修改权限,也不会代替开发者去申请权限。

这里尤其需要小心依赖带来的配置差异:工程可能具有多个 HAP/HSP,构建工具和依赖包也可能影响最终产物。本脚本读取的是一个源文件,不能宣称覆盖最终安装包的权限全集。若目标是发布级核验,应结合实际构建产物、各模块声明和最终包检查,并由负责人审核基线变更。

五、让界面表达“需复核”,不要冒充“已审核”

手机演示图显示任务 PL-042、声明 3、基线 2、多余 1、字段缺失 1,并把 ohos.permission.MICROPHONE 单独标红。页面给出的结论只有“需复核”。这正好对应前面的脚本输出,而不是给出“应用市场审核未通过”的伪真实记录。图中 14:40 是为了展示固定状态使用的样例时间;DevEco 演示图中使用了另一组样例日志时间,两者是不同时间的静态演示画面,不是同一次真机执行轨迹。

对审题和技术负责人而言,可把预检结果拆成三个动作。先查 MICROPHONE 是否仍对应本版本功能;如果不再需要,删除源声明并检查依赖包是否也引入相关权限。如果需要,确认 reason、usedScene、资源多语言适配、权限申请时机和用户可感知的业务场景。最后重新运行扫描,把“为什么保留”写在代码评审记录里,避免下一个版本再次追问同样的问题。

本方案还应区分“团队允许”和“平台允许”。团队基线里列了 CAMERA,不意味着用户已经授权,也不代表所有用户流程、权限弹窗或隐私政策都符合最新审核要求。真实提交仍要以华为开发者联盟当期指南、应用的实际功能和后台检查结果为准。

六、可复现的自检步骤与边界

对这个静态输入,开发者可以检查脚本理论上应输出:declared=3 baseline=2、extra=1 missingFields=1、REVIEW_REQUIRED。再把测试配置中的 MICROPHONE 删除,预期变为 2、2、0、0;把它加回并补齐字段,字段缺失项可变成 0,但“非基线”仍为 1。这些是基于示例代码的逻辑推导,并非本文已经运行 Node 程序得到的实测日志。

真正落地时,我会让 CI 产出 report.json,同时在合并请求描述中记录维护人、功能变更和基线变更原因。避免把权限检查做成一个神秘的分数。脚本越简单,越应该把“不知道的事”说清楚:它不追踪运行时调用,不判断隐私文案是否充分,不判定应用市场是否通过,也不预测受限权限的资质审批结果。

七、参考与适用范围

本文主要依据华为开发者联盟《在配置文件中声明权限》文档(2026-09-08 版本内容),以及相关使用场景示例进行规则设计;json5 是 Node.js 社区解析依赖,不是 HarmonyOS SDK API。参考:

  • https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/declare-permissions
  • https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-v5/web-rtc-V5
  • https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/ide-hmos-hvigor-build-profile-app

本文素材为独立设计的静态演示和待执行代码片段,未声称通过 DevEco 编译、脚本执行或真实市场审核。适用于搭建发布前的本地预警环节,不是一次性解决所有权限与隐私合规问题的产品。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐