权限与隐私封面

权限合规最常见的问题,不是代码里偷偷申请了大量敏感权限,而是“声明、行为和说明”没有对上:module.json5 保留了网络权限,源码却完全使用本地题库和 Preferences;应用介绍写“离线可用”,安装包仍带有不必要的网络能力;设置页能清除学习数据,但隐私说明没有列出收藏、笔记、错题和考试历史;未来增加相机或位置功能时,只设计授权成功路径,没有处理拒绝和永久拒绝。

中国方言题库的真实源码目前呈现出一个很清晰的审计结果:47 个 ArkTS 源文件中没有发现 HTTP、Web、Socket、上传、下载、账号、广告、推送、分析、相机、麦克风、定位或通讯录调用;题库数据来自本地 mock,用户收藏、笔记、错题、进度、考试历史和学习设置保存在本机 Preferences;根包没有运行时第三方依赖。但 entry/src/main/module.json5 仍声明了 ohos.permission.INTERNET。这意味着“功能是本地的”与“安装包具备网络权限”之间存在可以消除的不一致。

本文面向 HarmonyOS 5.0 及以上版本,结合 module.json5EntryAbilityUserDataManager、设置页和本地题库真实实现,讲清如何完成权限最小化、隐私字段盘点、删除路径核验与未来拒绝路径设计。本文唯一核验标记:权限声明必须能被真实功能解释,隐私说明必须能被真实数据验证

一、先从清单而不是页面文案开始

权限审计的第一证据是模块清单。当前 entry/src/main/module.json5 包含:

"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
]

同时声明支持:

"deviceTypes": [
  "phone",
  "tablet",
  "2in1"
]

INTERNET 是唯一声明的权限。清单中没有相机、麦克风、位置、联系人、日历、媒体文件或通知权限,也没有对应的权限用途说明字段。

仅看清单还不能得出“应用使用网络”的结论。权限声明表示应用具备调用相关能力的条件,不代表代码一定调用。下一步必须把它与源码和依赖一起核对。

二、源码侧没有找到网络调用

对 entry、librarya 和 libraryb 的 ArkTS 源码执行聚焦检索后,没有发现:

HTTP 请求
fetch
Web 组件
Webview
Socket
文件上传或下载
云数据库
账号登录
广告 SDK
分析 SDK
推送 SDK
在线图片地址

oh-package.json5dependencies 也是空对象,只有测试相关 devDependencies。应用自己的两个 library 模块由工程内部引用,不是在线运行时 SDK。

因此,从当前可见源码看,INTERNET 没有对应功能。最小权限原则下,更合理的处理是从 requestPermissions 中移除它,而不是为了“以后可能用到”先保留。

这里需要保持证据边界:静态检索能说明当前源码与声明不一致,但不能替代对最终 release 包和所有二进制依赖的审计。如果构建产物还打入了未被源码检索覆盖的原生库或三方 SDK,应继续检查包内容;当前根依赖与模块结构没有显示这类能力。

从权限声明到发布材料的核验流程

三、为什么一个普通网络权限也不该无理由保留

有人会认为 INTERNET 不弹运行时授权框,保留也没有用户体验成本。但应用商店审核和隐私一致性关心的不只是弹窗,还关心“安装包能做什么”和“产品声称做什么”。

对一个本地题库应用,不必要的网络权限会带来三个问题:

  1. 与“数据保存在本机”或“离线使用”的产品说明冲突。
  2. 增加审核人员对数据是否上传、是否存在在线服务的疑问。
  3. 未来排查隐私行为时,需要额外证明网络能力没有被使用。

权限最小化不是为了少写一行配置,而是减少需要解释、测试和披露的能力边界。没有真实网络功能时,删除权限是最简单、最可验证的方案。

四、当前本地数据到底有哪些

UserDataManager 使用:

preferences.getPreferencesSync(
  context,
  { name: 'dialect_quiz' }
)

建立本地 Preferences。持久化键包括:

favoriteRecords
noteRecords
wrongRecords
bankProgress
examHistory
chapterProgress
dailyReminderTime
examDurationSec
autoNextQuestion

这些键对应的数据是:

收藏记录:题目 ID、题库 ID、创建时间
笔记记录:题目 ID、题库 ID、用户输入内容、更新时间
错题记录:题目 ID、题库 ID、答错时间
题库进度:已答数、正确数、最近章节、更新时间
章节进度:章节已答数和正确数
考试历史:分数、题量、正确数、用时、完成时间
学习设置:提醒时间、考试时长、自动下一题

其中笔记内容属于用户主动输入的数据;学习记录虽然不一定是敏感个人信息,也属于用户产生的数据。隐私说明应至少告诉用户数据种类、用途、存储位置、是否上传、如何删除。

五、本地存储不等于“没有隐私问题”

数据不上传可以显著降低风险,但仍需要回答:

数据为什么被保存
保存在什么位置
应用卸载后如何处理
用户能否主动清除
是否会被备份或迁移
是否与账号关联
是否提供导出

当前源码没有账号体系、云同步或跨设备同步,数据只通过本地 Preferences 读写。文章不会把它描述成加密数据库,也不会声称已经实现备份、导出或恢复。

UserDataManager 使用普通 Preferences,没有显示自定义加密。收藏、错题、进度这类低敏学习数据通常可以使用 Preferences,但隐私说明不能写成“所有数据均加密存储”,除非实现和验证了对应加密能力。

六、设置页已经提供删除入口

SettingsPage 展示收藏、笔记、错题、学习进度和考试历史数量,并提供“清除所有学习数据”。删除不是一次误触即执行,而是两步确认:

private requestClearAll(): void {
  if (this.pendingClear) {
    this.clearAllLearningData()
  } else {
    this.pendingClear = true
    this.showTip('再次点击红色按钮确认清除所有学习数据')
  }
}

第二次点击后,页面依次清空:

this.favRecords = UserDataManager.clearFavorites()
this.noteRecords = UserDataManager.clearNotes()
this.wrongRecords = UserDataManager.clearWrong()
this.progressList = UserDataManager.clearProgress()
this.chapterProgressList =
  UserDataManager.clearChapterProgress()
this.examHistory = UserDataManager.clearExamHistory()

这条路径覆盖了主要学习数据。但三个设置值没有在“清除所有学习数据”中恢复默认:

dailyReminderTime
examDurationSec
autoNextQuestion

因此按钮文案“所有学习数据已清除”基本符合收藏、笔记、错题和进度语义,却不等于“清除应用全部本地数据”。隐私说明和 UI 文案应明确删除范围,避免把偏好设置也描述为已删除。

七、删除成功反馈与持久化失败边界

clearAllLearningData() 最后会显示:

this.showTip('所有学习数据已清除')

UserDataManager.persist() 的异常处理是:

try {
  prefs.putSync(key, JSON.stringify(value))
  prefs.flushSync()
} catch (_) {}

写入失败被静默吞掉,方法也不返回成功或失败状态。页面会立即把 @StorageLink 数组设为空,所以当前界面看起来已经删除;如果磁盘写入失败,重新启动后旧数据仍可能恢复。

这不是说正常路径一定失败,而是删除反馈没有和持久化结果绑定。隐私删除属于需要可靠确认的操作,建议让清除方法返回布尔值或明确结果对象:

interface ClearResult {
  success: boolean
  failedKeys: string[]
}

只有所有目标键落盘成功后显示“已清除”;失败时保留可重试提示。这样删除承诺才与真实持久化结果一致。

八、清除多类数据不是原子事务

设置页按键逐个调用清空方法。每个方法都会 putSyncflushSync。如果清除收藏、笔记成功,清除错题时失败,就会出现部分数据已删、部分仍在的状态。

Preferences 适合轻量键值,不代表多个独立写入自动具备事务。对于“全部清除”这种跨键操作,可以考虑:

方案一:使用 Preferences 的整体 clear 能力
方案二:统一写入后只 flush 一次
方案三:增加清除版本标记并在启动时完成恢复

选择哪种方式要依据官方 API 和现有数据迁移策略。关键是页面不能把多步操作当成天然原子操作。

九、当前权限没有运行时拒绝路径

因为当前清单只有 INTERNET,源码也没有调用权限管理 API,所以应用不会出现相机、位置、麦克风那类运行时授权弹窗,也没有“用户拒绝后怎么办”的代码路径。

这本身与当前离线题库功能一致:如果移除无用 INTERNET,应用可以做到不申请任何权限,用户也无需面对拒绝场景。

文章标题中的“拒绝路径一致”不能被误解为当前源码已经实现权限拒绝 UI。准确说法是:现有功能不需要敏感权限,因此最佳拒绝路径是“不发起不必要的请求”;未来新增能力时,必须把拒绝路径与功能一起交付。

十、未来新增权限时先写功能契约

假设未来增加“朗读录音”功能,可能需要麦克风权限。开发前应先定义:

触发动作:用户主动点击“开始录音”
用途说明:录制本次方言练习
处理位置:仅本机分析或明确的在线服务
保存周期:即时处理后删除,或由用户管理
拒绝行为:仍可使用文字题与其他题库功能
再次请求:在用户再次点击功能时说明原因
永久拒绝:提供前往系统设置的明确入口

权限不能在应用启动时无场景地批量申请。应在用户触发相关功能时,先解释用途,再调用系统授权。

十一、拒绝不能把整个应用锁死

如果某个新功能只占应用的一部分,用户拒绝权限后应降级该功能,而不是让首页无法进入。以录音为例:

允许
  -> 进入录音流程

拒绝
  -> 提示“未获得麦克风权限”
  -> 保留文字练习、收藏和考试

永久拒绝
  -> 说明可在系统设置中开启
  -> 不循环弹窗

拒绝提示必须和功能真实依赖一致。不要用“为了提供更好的服务”这种泛化文案,也不要把非必要权限包装成必需条件。

十二、module.json5 只是权限真源之一

完整一致性至少需要核对四层:

模块清单
  -> 声明了哪些能力

运行时代码
  -> 实际调用了哪些能力

功能与 UI
  -> 用户从哪里触发、拒绝后怎样

隐私与上架材料
  -> 如何描述用途、数据、处理位置和删除

任何一层不一致都可能造成审核或信任问题。例如清单删除了网络权限,但隐私政策仍写“通过服务器同步题库”;或者清单保留定位,页面却没有任何位置功能入口。

权限与隐私一致性的职责结构

十三、应用说明应该怎样描述当前版本

结合真实代码,当前版本可以准确描述为:

题库内容内置于应用
收藏、笔记、错题、学习进度和考试历史保存在本机
无需登录账号
不提供云同步
用户可在设置中清除主要学习数据

不能未经验证宣称:

所有数据均加密
支持跨设备同步
支持云端备份
不包含任何网络权限
清除按钮会删除全部偏好

其中“不包含任何网络权限”必须等 module.json5 移除 INTERNET、重新构建并检查最终包后才能写入发布材料。

十四、隐私说明至少应列出哪些字段

对当前本地题库,隐私说明可以按数据用途组织:

收藏与错题

用于在本机展示收藏列表、错题复习和统计数量。数据包含题目 ID、题库 ID 与记录时间。

学习笔记

用于保存用户对题目的自定义文字。笔记只在本机展示,不应写成公开内容或社区内容。

学习进度

用于计算已答题数、正确率、章节完成度和最近练习状态。

考试历史

用于展示分数、正确数、题量、用时与完成时间。

学习设置

用于记住提醒时间、考试时长和是否自动进入下一题。当前代码只保存“提醒时间设置”,没有看到通知权限或定时通知实现,不能宣称已经发送系统提醒。

十五、提醒设置暴露出的功能说明边界

设置页允许选择“每天 09:00”“每天 12:00”“每天 21:00”或关闭,并把值保存到 Preferences。但源码检索没有发现通知权限、通知请求或后台提醒调度。

因此当前实现的真实能力是“保存每日学习提醒时间偏好”,不是“系统会在指定时间发送通知”。如果 UI 对用户展示为已设置提醒,可能形成期待差异。

解决方式有两个:

如果暂不实现通知
  -> 把文案改为“计划学习时间”
  -> 不声称会系统提醒

如果实现通知
  -> 增加真实通知能力
  -> 核对通知权限、授权流程、拒绝路径
  -> 更新隐私和功能说明

不能仅凭保存了时间字符串就把通知能力写入上架材料。

十六、页面路由参数也要按数据最小化检查

应用使用 router.pushUrl() 在页面间传递题库 ID、章节 ID、练习模式和答题记录等参数。考试结果页会把答题记录 JSON 传给错题解析。

这些数据在同一应用内用于页面导航,没有看到上传行为。但仍应遵守最小化:

只传目标页面需要的数据
避免在日志中打印完整用户笔记
避免把大数组长期挂在路由历史
对 JSON 解析失败提供默认值

当前 ExamResultPage 对 records 的 JSON 解析使用 try/catch,失败时保留空数组。它是输入容错,不涉及系统权限。

十七、日志也属于隐私审计范围

EntryAbility 使用 hilog 记录生命周期和错误原因。当前日志主要是 Ability 启动、窗口加载和避让区错误,没有输出收藏、笔记内容、题目答案或用户标识。

后续开发时应保持这个边界:

可以记录错误类型和流程节点
不要记录完整笔记正文
不要记录签名秘密或账号凭证
不要记录权限授权结果之外的敏感数据
release 版本避免无必要调试日志

权限合规不仅看清单,也看敏感数据是否通过日志、缓存或错误上报外泄。

十八、第三方依赖为什么必须单独审计

当前根 dependencies 为空,这降低了隐私审计复杂度。未来一旦加入统计、广告、推送、崩溃上报、在线语音识别或账号 SDK,不能只看应用自己是否调用网络。

需要核对:

SDK 初始化时机
是否自动采集设备信息
是否启动即联网
域名和服务器位置
个人信息字段
隐私政策披露
用户同意前是否运行
是否可关闭或延迟初始化

SDK 可能间接引入权限或网络行为。上架材料中的 SDK 列表、隐私标签和包内依赖必须一致。

十九、移除 INTERNET 后怎样验证

修改清单后应重新构建 release 包,并执行最小烟测:

安装应用
冷启动进入首页
浏览题库和分类
随机练习与模拟考试
保存收藏、笔记和错题
退出并重新启动,确认本地恢复
进入设置并清除学习数据
无网络环境下重复核心流程
卸载应用

还应检查构建产物中的权限清单,确认不是只改了源码文件却仍使用旧包。无网络环境测试可以验证核心流程确实不依赖在线资源,但不能替代清单检查。

二十、权限审计的自动化清单

可以把以下检查加入发布前脚本或 CI:

读取 module.json5.requestPermissions
扫描 @kit.NetworkKit、Web、Socket 等导入
扫描权限请求 API
扫描第三方依赖
扫描远程 URL 与域名
列出 Preferences / RDB / 文件存储键
核对隐私说明中的数据类别
核对删除入口和真实删除键
核对 release 包权限

自动化不能判断文案是否误导,但能快速发现“清单有权限、源码无调用”或“源码调用能力、清单没有声明”的明显偏差。

二十一、当前项目最小整改建议

按风险和成本排序:

1. 确认最终包确实没有网络功能或网络 SDK
2. 从 module.json5 移除无用 INTERNET 权限
3. 重新构建并检查 release 包权限
4. 将提醒时间文案与真实通知能力对齐
5. 明确“清除学习数据”不包含三个设置偏好
6. 让持久化清除返回真实成功/失败
7. 在隐私说明列出本地数据类别和删除路径
8. 保持上架隐私标签、功能说明与包行为一致

如果后续确认确实有在线更新题库的需求,则不能简单移除网络权限;应实现真实网络服务、加载失败和离线回退,并同步更新隐私材料。当前源码没有这条链路。

二十二、拒绝路径的测试用例模板

当前版本没有敏感权限,因此不需要运行此模板。未来新增权限时可直接复用:

首次点击功能,授权允许
首次点击功能,授权拒绝
再次点击,解释用途后重试
永久拒绝,提供系统设置入口
从系统设置返回,重新检查状态
授权后关闭权限,功能降级
拒绝权限时其他页面仍可使用
应用重启后不循环弹窗

每条都应验证 UI 文案、权限状态和业务结果,而不是只确认系统弹窗出现过。

二十三、隐私一致性不是一次性文档工作

每次增加以下能力,都要重新审计:

联网
账号
云同步
相机
麦克风
位置
通知
广告
分析
反馈上传
第三方 SDK
数据导出或备份

权限、隐私和功能是同一份产品契约的不同表现。代码变化后,清单、隐私政策、用户协议、应用详情和审核备注都要一起更新。

二十四、结语

中国方言题库当前核心流程是本地题库加本地 Preferences,用户无需登录,也没有可见的网络调用和敏感权限请求。真正需要修正的是 module.json5 中未被真实功能解释的 INTERNET 权限,以及“提醒”和“清除全部”文案与实际能力的边界。

可以把整套审计浓缩成一句话:权限声明必须能被真实功能解释,隐私说明必须能被真实数据验证。不需要的权限直接移除,需要的权限在用户触发场景中申请;本地数据如实列出用途和删除范围;未来增加敏感能力时同时交付允许、拒绝、永久拒绝和降级路径。只有清单、代码、页面和上架材料互相证明,权限与隐私才真正一致。

AI 辅助声明:本文在结构整理和语言润色环节使用了 AI 辅助;权限声明、源码调用、本地数据键、删除范围和提醒能力边界均依据中国方言题库真实工程文件复核。

Logo

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

更多推荐