HarmonyOS Node.js:多模块权限理由缺失与重复校验【共创稿事节】
鸿蒙工程拆成entry、feature和gallery模块之后,权限声明容易出现一种不太醒目的配置差错:module.json5里写了reason,文字看起来也规范,但它指向的字符串资源实际上不存在。再叠加从老模块复制粘贴的习惯,同一模块内还可能出现同名权限重复声明。这类问题不应该等打包上传才被人肉发现,改一处资源引用就要重新走整条交付链,成本并不低。
这次做一个PermReasonGate / PermReasonGate的构建前检查器。扫描的是工程中显式指定的三个模块:entry、feature、gallery。构造用例预期为entry PASS、feature MISSING_REASON、gallery DUPLICATE_NAME,最终PASS 1/3 · BLOCKED。它只衡量团队定义的资源引用完整性与重复声明,绝不把检查通过写成应用市场批准发布。本文使用的文件路径与字段形式对应Stage模型的常见工程结构,是否必须填写某权限理由仍以权限级别和官方最新要求为准。

一、从配置漂移而不是权限大全开始
不要上来就维护一张“全部权限都必须有reason”的表。不同权限的授权方式、条件字段和使用场景并不相同。本文检查器采用更保守的原则:当某个声明已经使用$string:xxx资源引用时,验证它能否在模块预期资源表中解析;同一requestPermissions数组里的权限名不应重复出现。没有reason的声明仅提示需要业务复核,不直接定性为违规。
目录以典型工程为例:每个模块的src/main/module.json5保存声明,src/main/resources/base/element/string.json保存基础字符串。实际多语言资源可能在locale等资源目录中覆盖基础项,正式项目可扩展扫描集并检查语言包缺项。本文的最小工具只检查base层,并不等同于覆盖所有运行时资源选择逻辑;如果该项目允许从公共资源模块引用,必须把解析规则显式配置进去,不能简单地把模块路径当成唯一真相。
一个容易被忽略的细节是重复作用域。不同模块声明相同权限,不应被本工具直接判为重复,因为它们可能各自需要访问能力。这里的DUPLICATE_NAME特指同一个module.json5的同一个声明数组里两次出现相同name。把跨模块一致性问题与单模块冗余写法区分开,报告才不会堆满误报。
二、输入不可信,先确保解析边界
因为JSON5支持注释与尾逗号,不能拿JSON.parse直接解析任意module.json5。脚本使用社区的json5包进行解析,Node.js标准库负责文件读取;依赖需要在构建环境中明确安装并锁定版本。本工具不请求设备权限、不执行module中的脚本,也不会写回开发者的配置。读取失败时返回带模块名的错误,而不是自动补一个空数组让检查“顺利通过”。
// scripts/permission-gate.mjs;工程需安装并锁定 json5 开发依赖
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import JSON5 from 'json5'
async function readModule(root, name) {
const main = path.join(root, name, 'src', 'main')
const manifestText = await readFile(path.join(main, 'module.json5'), 'utf8')
const parsed = JSON5.parse(manifestText)
const resourceText = await readFile(
path.join(main, 'resources', 'base', 'element', 'string.json'), 'utf8')
const entries = JSON.parse(resourceText).string ?? []
const keys = new Set(entries.map((item) => String(item.name)))
return { permissions: parsed.module?.requestPermissions ?? [], keys }
}
main位置由用户传入的工程根目录与固定模块清单拼成,模块名不能直接使用未经清洗的外部请求参数,否则容易造成路径跳转。本文固定为三个受信任模块名,命令行只传工程根路径。字符串资源用Set保存键名以便做精确查找;资源内容是否为空、是否包含合理文案,需要后续专门的文案审查步骤,不能用“键存在”替代“说明合适”。
这个读取函数是示例流程骨架。若项目存在JSON5扩展字段或资源目录由构建配置调整,扫描路径应从项目配置生成,而不是到处硬编码。尤其不要在一个模块的资源缺失时偷偷去另一个模块猜同名键;这种“帮开发者修复”的行为会把配置错误变成更隐蔽的偶发问题。
三、将差错归因到模块和权限名
具体判定函数不需要了解设备型号,也不需要调用任何HarmonyOS系统API。它逐条读取权限声明,维护本模块内部的seen集合,并检查reason是否明确引用资源键。为了方便在CI上理解,返回结果中同时保留问题码与权限名,报告可以精确定位,而不是只输出一个笼统的失败数量。
function inspectPermissions(permissions, keys) {
const seen = new Set()
const issues = []
for (const item of permissions) {
const name = String(item?.name ?? '').trim()
if (!name) { issues.push('EMPTY_NAME'); continue }
if (seen.has(name)) issues.push('DUPLICATE_NAME')
seen.add(name)
const reason = typeof item.reason === 'string' ? item.reason.trim() : ''
if (reason.startsWith('$string:')) {
const key = reason.slice('$string:'.length)
if (!key || !keys.has(key)) issues.push('MISSING_REASON')
} else if (reason) {
issues.push('REVIEW_INLINE_REASON') // 提醒人工复核,不等于系统报错
}
}
const hard = issues.filter((code) => code !== 'REVIEW_INLINE_REASON')
return { code: hard[0] ?? (issues.length ? 'REVIEW' : 'PASS'), issues }
}
这里故意没有用“首条异常出现就抛错”的写法:如果同一个模块里同时存在缺资源和重复声明,脚本应该尽量报告全部结构性问题。REVIEW_INLINE_REASON属于提醒,不直接阻断;MISSING_REASON只对显式资源引用生效,不会因为某条权限没有配置reason就随意断言违规。权限实际申请时机、usedScene配置、敏感权限信息和上架材料仍需按官方文档逐项核实。
字符串查找是精确匹配,大小写不同就视为不同键。不要随意将资源名转换成小写后再查找,那样会让构建成功与运行时资源解析产生分歧。额外可以将声明重复时的两处行号一起报告,不过需要带位置信息的解析器;这里先保留稳定、可读的错误码,避免假装拿到了源文件的准确行号。
四、用三个模块覆盖成功与失败路径
为了让输出可复现,构造一组三模块样例。entry声明相机权限并引用base表中存在的camera_reason,所以返回PASS;feature声明定位权限,引用location_reason,但是该模块base资源表没有对应名字,因此返回MISSING_REASON;gallery模块在同一个声明数组里重复列出相机权限,虽然资源键都存在,仍返回DUPLICATE_NAME。注意这些只是测试用权限名称,不表示此示例实际调用相机或定位设备。
const fixtures = [
{ name: 'entry', permissions: [
{ name: 'ohos.permission.CAMERA', reason: '$string:camera_reason' }
], keys: new Set(['camera_reason']) },
{ name: 'feature', permissions: [
{ name: 'ohos.permission.LOCATION', reason: '$string:location_reason' }
], keys: new Set(['feature_name']) },
{ name: 'gallery', permissions: [
{ name: 'ohos.permission.CAMERA', reason: '$string:camera_reason' },
{ name: 'ohos.permission.CAMERA', reason: '$string:camera_reason' }
], keys: new Set(['camera_reason']) }
]
const reports = fixtures.map((m) => ({ module: m.name,
...inspectPermissions(m.permissions, m.keys) }))
const pass = reports.filter((item) => item.code === 'PASS').length
console.log(reports.map((r) => `${r.module}: ${r.code}`).join('\n'))
console.log(`PASS ${pass}/${reports.length} · ${pass === reports.length ? 'READY' : 'BLOCKED'}`)
这段可由本地Node.js直接执行验证,不需要连接云端或登录开发者账号。正式文件扫描则把fixtures替换为前文readModule得到的数据,将出错模块与原始文件路径写入报告,并在CI脚本中根据严重程度设置退出码。只在处理结构性硬错误时返回非零状态更可控,避免一条提醒性质的文案复核把整个发布流水线卡死。

五、做审计时最怕报告和界面不一致
画面里列出的三个模块必须与文本示例一致,不能出现“明明只有两条问题却画了四个红叉”。工具本体应当在开发电脑或CI中执行,手机页面只是可选的结果展示层。
我建议日志格式固定为模块名 + 结果码 + 资源键,其中敏感信息不写入日志。错误码应该稳定,中文说明可以根据团队语言偏好调整。CI上输出一个machine-readable JSON,同时输出简洁的人可读摘要,便于审核同事不用翻源码就能找到坏配置。对尚未完成的模块读取,脚本要显示IO_ERROR或PARSE_ERROR并阻断,不允许用PASS 0/0误当成扫描完成。

六、与官方审核之间留出清楚的边界
官方资料可以核实module.json5里的requestPermissions字段及reason资源引用形式,资源配置文档也描述了resources/base/element/string.json的结构。但是是否属于用户授权权限、是否需要usedScene、在应用市场提供何种隐私说明,不能从“资源是否存在”一项推导。工具只负责把确定性的配置漂移拦在本地:键缺失、声明重复、文件不可解析;其余权限适用性交由官方规则和项目实际调用场景复核。
资源配置可能随模块拆分、产品地区和多语言版本变化。实际扩展时,应建立一张清晰的可配置模块清单,再决定检查基础资源还是全部locale目录。不要仅根据一个构造项目的目录布局,把新的CI脚本强行套进所有仓库。对于权限声明已经重复多年的项目,建议先以只读报告模式运行一个版本,确认团队认定的异常和真正在工程里出现的差异,再打开阻断开关。
最后本轮结果不是“所有权限符合要求”,而是更窄、更可以复现的结论:**三个模块里一个通过,另两个分别有缺失资源引用与单模块重复声明,整批BLOCKED**。只要报告中的模块名、错误码、示例文件与界面数据保持一致,维护人员就可以把一次模糊的“上架前检查”变成几分钟内可定位的工程修改。
参考资料:华为官方权限配置示例:https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/graphics-accelerate-gamebuddyservice-prepare ;华为ArkUI资源配置示例:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-column ;AppGallery隐私说明配置:https://developer.huawei.com/consumer/es/doc/app/agc-help-release-app-privacy-desc-0000002313477969%E3%80%82
更多推荐


所有评论(0)