声明与使用怎么对账、最小权限集怎么算才不自相矛盾、审核必查的 reason 和 usedScene 怎么补齐——这篇文章用一个纯 JS 的权限审计工具,把这三个问题一次讲透。

仓库地址:https://atomgit.com/lqjmac/react-native-permission-guard
版本地址:https://atomgit.com/oh-react-native/ohos_react_native/tree/0.84-main
三方库框架:https://atomgit.com/oh-react-native

0. 环境与实测设备

项版本/型号
DevEco Studio26.0.0.851(HarmonyOS SDK 7.0.0 / API 26)
React Native0.84.1
RNOH0.84.3(@react-native-oh/react-native-harmony,示例宿主同版本)
react-native-harmony-permission-guard1.0.0(本仓库)
Node.js20.19.5(审计核心是纯 JS,不依赖 RN 运行时)
测试node:test 14/14:module.json5 解析(引号 key、嵌套 usedScene)、最小集、隐含权限、风险分、Markdown、最小清单
实测环境HUAWEI ALN-AL00,HarmonyOS 7.0.0.109(SP6C00E105R4P3)

1. 权限清单为什么越来越长?——这个问题是怎么找上我的

起因其实很简单。接手一个 RN 鸿蒙应用做发布前自查,我打开 module.json5 看了一眼 requestPermissions——六个权限,INTERNET、定位、相机、麦克风、通讯录、分布式同步,心里想"都是常见项,应该没问题"。直到应用市场审核的打回意见摆到面前:缺 reason、缺 usedScene;再一问业务,麦克风和通讯录根本没有任何功能在用——它们是两年前从别的工程抄模板抄过来的。

想清理,才发现这件事在 RN 里格外难:RN 的权限调用通常发生在原生模块或 HAR 里,纯 JS 侧的代码根本看不出"这个页面到底用了哪些系统能力"。人工对着源码猜,猜不全;只跑静态扫描,扫不到原生层;等应用市场打回再改,一轮审核一周就过去了。还有一个更隐蔽的坑:用了精确定位却没申请 APPROXIMATELY_LOCATION,授权流程不完整——这类"配套权限"的缺失,靠肉眼基本不可能发现。

所以这个题目的边界其实很清楚:把"声明"和"使用"放在一张表里对账,具体拆成三件事:

  • 三路输入怎么合一(module.json5 声明、使用台账、源码证据扫描,这是核心);
  • 最小权限集怎么算才不自相矛盾(隐含权限、系统 Picker 的证据判定);
  • 结果怎么变成可执行的动作(六类 finding、可粘贴的最小清单、退出码卡 CI)。

下面是干完之后的完整记录,包括我踩的每一个坑。

2. 效果展示

按老规矩,先给效果再讲原理。示例应用的报告页分四个 Tab——总览、声明、发现、清单——正好是审计结论的四个层次。报告数据来自 example/fixtures/bad 里一份故意写坏的 module.json5(六条权限声明,问题齐全),宿主 example/harmony 里的 module.json5 则是修正后的可打包版本,两相对照。

第一页是总览,结论一行都不含糊:

在这里插入图片描述

右上角红色"未通过",核心数字是"声明 6 条 → 建议保留 4 条",下面一行给出动作明细:“移除 3 · 补充 1 · 精简 50% · 风险分 26 → 14”,再往下是红字"3 项高危,CI 会失败"和依据——“6 条声明中 3 条有使用证据;最小集合 1 条补充权限”。"待处理"区把三条高危逐条列出:声明了未使用的权限 MICROPHONE、READ_CONTACTS、DISTRIBUTED_DATASYNC(都是 PG-PRM-001)。页面底部是三个明细入口:声明清单 6 条、审计发现 7 条、最小权限清单 4 条。

第二页是声明清单,每条权限都带上了它的"供词":

在这里插入图片描述

六条声明逐条列出,右侧是结论。三条保留的都有证据链:INTERNET(reason 未填,但证据是 api-client、App.tsx#api)、LOCATION($string:location_reason,证据 nearby-store、App.tsx#nearby)、CAMERA(证据 scan-code、App.tsx#scan);三条标红"建议移除"的,理由清一色是"无使用证据"——MICROPHONE、READ_CONTACTS 带着写好的 reason 却没人认领,DISTRIBUTED_DATASYNC 连 reason 都没填。注意 INTERNET 的"未填写 reason"只是它自己的小问题(PG-PRM-003 类),不影响"保留"的结论——用没用和材料齐不齐,是两本账。

第三页是审计发现,七条按级别排序:

在这里插入图片描述

高危三条全是 PG-PRM-001,但每条的解释都带着这个权限的"人设":麦克风——“录音/语音通话才需要,必须逐次说明用途”;通讯录——“能用联系人 Picker 就不要申请全量读取”;分布式同步——“只有多设备协同才需要,普通 App 不应申请”。中危四条是材料问题:PG-PRM-003 缺 reason(“user_grant 权限必须写清楚’为什么需要’,否则应用市场审核与系统弹窗都会出问题”)、PG-PRM-004 缺 usedScene(“决定权限在哪个 Ability/场景生效,缺省会扩大授权范围”)。Tab 上的过滤计数(全部 7 / 高危 3 / 中危 4)和内容严格对得上。

第四页是清单——审计的最终交付物:

在这里插入图片描述

一段可直接粘贴的 requestPermissions:INTERNET、LOCATION、CAMERA、APPROXIMATELY_LOCATION 四条。下面一行说明很关键:“页面只列权限名;reason / usedScene 由 CLI 按授权类型补齐,user_grant 的 $string: 占位需补资源”——工具不越俎代庖替你编理由,但把骨架和占位都备好。"本次变更"区用 diff 风格列出移除三条、补充一条,页脚是版本与复现命令(node lib/cli/permission-audit.js --project example/fixtures/bad)——页面上的每一个数字都能用一条命令复现。

3. 对账到底从哪来:声明、台账、证据

写代码之前得先把原理捋好。

整个审计是"三进一出"的结构——三路输入并成一张对账表,输出一份可执行报告:

module.json5 的 requestPermissions  ──┐  当前声明了什么
permissions.usage.json(使用台账)  ──┤  哪个功能用了哪个权限(owner 维护)
src/**/*.{ts,tsx,js,jsx} 源码扫描  ──┘  API 调用痕迹(自动,按 API 判定)
                  │
    最小集 = 声明 ∩ 使用 → 并上"用了但没声明" → 补隐含权限
                  │
    六类 finding(PG-PRM-001~006) + 风险分(26 → 14)
                  │
    ├─ Markdown / JSON 报告(--fail-on 命中即退出码 2,卡 CI)
    └─ --write-patch:可直接粘贴的 requestPermissions 片段

为什么必须有台账? 这是 RN 场景的特殊性:权限调用通常发生在原生模块或 HAR 里,纯 JS 静态扫描根本看不到。台账(permissions.usage.json)是业务 owner 维护的"人的证词"——哪个功能用了哪个权限、证据在哪个文件。它不是多余的手工劳动,而是机器扫不到的那部分世界唯一的信息来源。

为什么扫描不能信到全信? 判定按 API 而不是按 import:cameraPicker、PhotoViewPicker、contact.selectContacts 这类系统 Picker 不需要权限,因此不计入证据——否则"换成 Picker 就能删掉的权限"会被最小集一直保留,最小化就永远最小不下去。最终的使用集合取台账与扫描的并集:台账防动态调用的漏报,扫描防台账的过期,两边互为兜底。

为什么要有风险分? 22 条权限目录每条标注 high / medium / low(高危 5 分、中危 3 分、低危 1 分),审计前后各算一次——示例里 26 → 14,一半的风险敞口直接来自砍掉那三条没人用的权限。但要清楚它的定位:风险分只是团队内部排序工具,不能替代应用市场审核。

各条路线对照一下:

方式依据RN 鸿蒙工程的盲区
人工 review module.json5记忆JS 侧看不到原生调用,全靠想
纯源码扫描import / API权限调用在原生 HAR 里扫不到;系统 Picker 误报
应用市场审核反馈提审之后打回才改,一轮审核一周起
商业合规平台全量重、贵、要接入,小团队用不起
本库 CLI + 报告页声明 + 台账 + 扫描三方对账最小集 + 风险分 + 退出码,纯 JS 离线秒级

能力边界照 README 的设计取舍说清楚:不自动改 module.json5(默认只出报告与建议,--write-patch 才生成片段——工具悄悄改权限清单这件事本身就是风险);不申请权限(审计与运行时分离,库不碰 abilityAccessCtrl,权限申请仍在应用自己的 ArkTS 代码里);风险分不是合规结论。了解了这个边界,你就知道它管的是"声明的账",不是"运行的权"。

4. 工程怎么搭:一个纯 JS 内核,三副面孔

先看整体结构。审计核心是纯 JS(不依赖 RN 运行时),同一份内核服务三个入口——CLI 给 CI、JS API 给工具链集成、报告页给人:

react-native-permission-guard/
├─ lib/
│  ├─ index.js / index.d.ts       # 审计核心:解析、对账、最小集、风险分、清单
│  └─ cli/permission-audit.js     # CLI:参数、源码扫描、退出码
├─ ohos/                          # ArkTS 权限目录 HAR(权限中文名/风险级/配套关系)
├─ test/                          # node:test 14 项 + fixture
├─ example/                       # RN 示例:App.tsx 四 Tab 报告页
│  ├─ fixtures/bad/               # 故意写坏的 module.json5 + 台账(复现 7 条 finding)
│  └─ harmony/                    # 完整 RNOH 宿主(module.json5 为修正版)
├─ docs/                          # 文章与截图
└─ package.json / CHANGELOG.md / README.OpenHarmony*.md

在这里插入图片描述

这次设计最关键的决定有三个:

审计核心是纯 JS。 不 import 任何 RN 或原生模块——node lib/cli/permission-audit.js 直接跑,node:test 直接测核心函数,任何装了 Node 的 CI 都能离线执行。鸿蒙相关的部分只剩一份 ArkTS 权限目录 HAR(22 条权限的中文名、授权类型、风险级、配套关系),它也只是数据。

不自动改 module.json5。 这是权限工具和普通格式化工具的分界线:权限清单是应用市场的审核材料,工具悄悄增删它等于替开发者做了安全决策。默认只出报告与建议;--write-patch 生成可粘贴片段,改不改、怎么改,决定权在人。

演练样本内置。 example/fixtures/bad 里是一份故意写坏的清单(六条权限,缺失 reason / usedScene、声明了未使用、缺配套权限的毛病全占),一条命令复现全部 7 条 finding——报告页上的每个数字都有来处。

对使用方来说,CLI 是这样的:

node node_modules/react-native-harmony-permission-guard/lib/cli/permission-audit.js \
  --project . \
  --format md \
  --out build/security/permissions.md \
  --fail-on high

JS API 是这样的:

const guard = require('react-native-harmony-permission-guard');

const declared = guard.parseModuleJson5(moduleJsonText);
const used = guard.parseUsageLedger(usageJson);
const report = guard.audit({ declared, used });

console.log(report.stats);      // { declared: 6, minimal: 4, ... }
console.log(report.findings);   // 逐条 finding
console.log(guard.buildMinimalManifest(report, report.declared));

5. 把代码讲透

5.1 最小集先行:一次排序,避免自相矛盾

audit() 里最值得学的是执行顺序——先算最小集,再出 finding。核心注释把理由写得清清楚楚:

// lib/index.js - audit()
// 先算最小集:声明 ∩ 使用,再并上"用了但没声明"的,最后补隐含权限。
// 先算出来,后面的 finding 才能和补丁保持一致,不会出现
// "建议移除 LOCATION"和"建议新增 APPROXIMATELY_LOCATION"同时出现。
const minimal = Array.from(new Set(
  declared
    .filter(function (item) { return usedNames.has(item.name); })
    .map(function (item) { return item.name; })
    .concat(Array.from(usedNames))
));

这个顺序问题我在第一版就踩了:先按声明逐条出 finding,再算补丁,结果 LOCATION 因为"台账没提"被建议移除,而配套规则又要求"补上 APPROXIMATELY_LOCATION"——报告自己打自己的脸。修复就是调整顺序:最小集是唯一事实源,所有 finding、风险分、补丁片段都从它派生。报告的每一部分互相咬合,这是审计工具可信的前提。

5.2 六类 finding:对账的六种破绽

声明与使用对账,破绽恰好六类,每类一个 PG-PRM-* 编号:

编号级别破绽动作
PG-PRM-001high/medium声明了未使用的权限remove
PG-PRM-002high使用了未声明的权限(真机会直接抛 201 权限拒绝)add
PG-PRM-003mediumuser_grant 权限缺 reason(审核打回、弹窗没文案)补材料
PG-PRM-004medium缺 usedScene(决定权限在哪个 Ability/场景生效,缺省扩大授权范围)补材料
PG-PRM-005medium缺少配套权限(如精确定位缺 APPROXIMATELY_LOCATION)add
PG-PRM-006medium未收录权限(目录里不认识,请人工确认)人工

每条的 severity 不是拍脑袋:001 按该权限自身的风险级定(麦克风这种高危权限没人用就是 high,低危权限没人用只是 medium);002 恒为 high,因为它上线即故障。detail 里写的解释也全部来自权限目录——"能用联系人 Picker 就不要申请全量读取"这类建议是目录数据的一部分,不是硬编码在 finding 里的字符串。

5.3 权限目录与隐含权限:先粗后精

权限目录(22 条)是审计的"字典",每条带四个字段——授权类型(system_grant / user_grant)、风险级、中文名、说明,部分条目还有 implies:

'ohos.permission.LOCATION': {
  grant: 'user',
  risk: 'high',
  label: '精确位置',
  note: '必须与 APPROXIMATELY_LOCATION 一起申请,先粗后精。',
  implies: ['ohos.permission.APPROXIMATELY_LOCATION']
},
'ohos.permission.APPROXIMATELY_LOCATION': {
  grant: 'user',
  risk: 'medium',
  label: '模糊位置',
  note: '城市级定位,优先用它替代精确位置。'
},

implies 驱动隐含权限补全:最小集里出现精确位置而模糊位置缺席时,自动补上并记录 impliedBy——这正是示例里"补充 1 条"的来源(申请了 LOCATION 却没申请 APPROXIMATELY_LOCATION,精确授权流程不完整)。找到不认识的权限也不瞎猜,PG-PRM-006 交人工确认——字典覆盖不到的,宁可麻烦人,不可假装懂。

5.4 风险分与可粘贴清单:按授权类型说话

风险分是三值加权:高危 5、中危 3、低危 1,声明集合和最小集合各算一次——riskBefore: 26 → riskAfter: 14,"精简 50%“的数字就是这两个分数的比值。它给团队排优先级用,README 特意声明"不是合规结论”。

最小清单(--write-patch)的生成规则体现的是对授权类型的尊重:

requestPermissions: [
  { name: 'ohos.permission.INTERNET' },
  { name: 'ohos.permission.LOCATION' },
  { name: 'ohos.permission.CAMERA' },
  { name: 'ohos.permission.APPROXIMATELY_LOCATION' },
]

system_grant 权限(如 INTERNET)不需要 reason/usedScene,清单就不写;user_grant 缺 reason 的生成 $string: 占位——占位而不是编文案,理由必须由开发者自己填(页脚说明原话:“user_grant 的 $string: 占位需补资源”);Ability 名用 --ability 配置而不写死 EntryAbility。生成的片段能直接粘进 module.json5,但粘完之后的审核材料,人还得过一遍。

5.5 台账与扫描:并集与证据分级

使用集合的组装是"台账 ∪ 扫描":台账条目形如 { id: 'scan-code', permission: 'ohos.permission.CAMERA', evidence: 'src/features/scan.tsx' },声明页展示的证据链(scan-code、App.tsx#scan)就是 id 加证据文件的组合。源码扫描按 API 判定:cameraPicker、PhotoViewPicker、contact.selectContacts 这类系统 Picker 不产生权限证据——这是 CHANGELOG 里专门修的一条,理由很硬:“否则’换成 Picker 就能删掉的权限’会被最小集一直保留”。两路证据的并集让动态调用(扫描看不到的)靠台账兜底,台账过期(人忘了更新)靠扫描兜底——单一路径都有盲区,交叉才是真相。

6. 我踩过的坑(每一条都有真实修复记录)

这一节可能是全文最值钱的部分。以下坑全部来自 CHANGELOG 修订记录里的真实修复。

坑一:系统 Picker 被当成权限证据。 源码扫描最初按 import 判定,扫到 cameraPicker 就把 CAMERA 标记为"在使用"——但 Picker 类 API 恰恰是不需要权限的那类,结果"换成 Picker 就能删掉的权限"被最小集永远保留,最小化永远做不彻底。修复:扫描按 API 白名单判定,Picker 不计入证据。证据判定错了,后面的账全是错的。

坑二:报告自相矛盾,移除与新增同时出现。 "建议移除 LOCATION"和"建议新增 APPROXIMATELY_LOCATION"在同一份报告里并列——移除是台账没提,新增是配套规则,两条规则各说各话。修复:先算最小集再派生 finding(见 5.1 的顺序调整)。审计报告内部打架,比漏报一条更毁信任。

坑三:module.json5 的解析野路子。 真实工程里的 json5 五花八门:usedScene 写在 name 前面、注释里带 }、key 用单引号、usedScene: {} 空对象——第一版解析器在这些写法上不是崩就是解析出半个清单。修复:解析器逐个补齐,并把 usedScene: {} 明确视为缺失(空结构和没写是一个意思)。解析用户的工程文件,按最野的写法做测试。

坑四:最小清单越俎代庖。 生成的补丁给 system_grant 权限也补上了 reason/usedScene——但系统授权类权限根本不需要这些字段,贴进清单反而是废材料;user_grant 缺 reason 时工具又替人编了一句通用文案。修复:按授权类型区别处理,user_grant 只生成 $string: 占位,文案由开发者自己填,Ability 名用 --ability 配置。工具可以备好骨架,不能替人写"为什么"。

坑五:台账过期,没人认领的权限其实是有人在用的。 台账是人维护的,功能迭代之后没人回头更新——仅凭台账会把"实际在用但没登记"的权限误判成可移除。修复:使用集合取台账与源码扫描的并集,两路证据互为兜底。单一路径都有盲区,交叉才是真相。

坑六:自动改清单的诱惑。 最省事的交付是工具直接把 module.json5 改好——但权限清单是审核材料,工具静默增删等于替开发者做了安全决策,出了问题连责任人都不清楚。修复:默认只出报告与建议,--write-patch 才生成可粘贴片段。权限这个领域,"不动手"比"高效"重要。

坑七:把风险分当合规结论。 演示时有人问"风险分 14 是不是就等于能过审"——不是。风险分只是 22 条目录三值加权的内部排序工具,应用市场审核有自己的checklist。修复:README 设计取舍里专门加了一条声明。工具能做的说够,做不到的说清,别让数字越权。

7. 现在怎么用:从一条命令到 CI 卡点

经过上面的整理,这个工程是"CLI + JS API + 报告页"三合一(npm 包名 react-native-harmony-permission-guard,版本 1.0.0,Apache-2.0)。安装:

npm install react-native-harmony-permission-guard

在 RN 工程根目录跑审计(--fail-on high 命中即退出码 2,流水线直接失败):

node node_modules/react-native-harmony-permission-guard/lib/cli/permission-audit.js \
  --project . \
  --format md \
  --out build/security/permissions.md \
  --fail-on high

参数一览:--project(工程根目录)、--usage(台账路径,默认 <project>/permissions.usage.json)、--format md/json、--out、--write-patch(输出最小 requestPermissions JSON5)、--fail-on none/low/medium/high、--ability(生成清单里的 Ability 名,默认 EntryAbility)。

台账是唯一要人维护的输入,格式三行一个功能:

{
  "features": [
    { "id": "scan-code", "permission": "ohos.permission.CAMERA", "evidence": "src/features/scan.tsx" },
    { "id": "nearby-store", "permission": "ohos.permission.LOCATION", "evidence": "src/features/map.ts" },
    { "id": "api-client", "permission": "ohos.permission.INTERNET", "evidence": "src/api/client.ts" }
  ]
}

想看报告长什么样,跑内置演练样本即可复现本文全部截图:

node lib/cli/permission-audit.js --project example/fixtures/bad --format json \
  --out example/permission-report.json --fail-on none

当前版本在 RN 0.84.1 / RNOH 0.84.3、DevEco Studio 26.0.0.851(HarmonyOS SDK API 26)环境下验证:npm test node:test 14/14 全绿——覆盖 module.json5 解析(引号 key、嵌套 usedScene)、最小集、隐含权限、风险分、Markdown、最小清单;示例应用四 Tab 报告页在真机运行,演练样本审计结论"声明 6 → 最小 4、风险分 26 → 14、7 条 finding(高危 3 / 中危 4)"与 CLI 输出逐项一致。权限最小化没有魔法——让每一项声明都有人认领,让每一项保留都有据可查,这件事靠的是对账,不是记忆。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐