鸿蒙存储权限高级精细化管控:应用沙箱存储隔离/私有目录访问/公共目录MediaLibrary/URI临时授权机制
·


一、前置思考
1.1 存储权限是隐私合规的"重灾区"
用户隐私合规审查中,存储权限是最容易被揪出来的点:应用申请了不必要的外置存储权限、把用户照片偷偷复制进私有目录、通过文件路径硬编码访问公共目录……上架审核被拒、隐私通报点名,多数与存储权限管控不当有关。
❌ 违规1: 仅仅为了保存一个导出文件就申请 READ_MEDIA
❌ 违规2: 用旧的 file:// 绝对路径访问公共媒体库
❌ 违规3: 把下载的图片直接存进应用私有目录且不加密
❌ 违规4: 授权过期后仍保留文件访问权
1.2 HarmonyOS 6.1.0 的存储权限体系
鸿蒙存储权限的核心思路是沙箱隔离 + 最小授权 + 临时授权 + 用户可控:
| 存储区域 | 访问方式 | 是否需要权限 |
|---|---|---|
| 应用私有目录 | 直接路径访问 | 无需权限 |
| 应用内 share 目录 | 直接访问 | 无需权限 |
| 公共媒体库(相册/音频/视频) | PhotoAccessHelper / MediaLibrary API | 动态申请或用户选择 |
| 公共文件目录(文档/下载) | FilePicker / 文件选择器 | 用户逐次授权 |
| 其他应用私有目录 | 完全禁止 | - |
1.3 本文路线
深入沙箱隔离机制、私有目录访问、MediaLibrary 公共目录授权、URI 临时授权四大块,给出存储权限工程化落地清单。
二、核心原理
2.1 应用沙箱存储隔离
每个应用拥有独立的沙箱目录,路径形如:
/data/app/el2/100/base/{bundleName}/haps/{moduleName}/files/
├── files/ 持久数据 (备份/迁移)
├── cache/ 缓存数据 (可被系统清理)
├── database/ 数据库文件
├── preferences/ 首选项文件
└── share/ 共享目录 (本应用各模块共享)
沙箱隔离的两层保障:
- 路径隔离:其他应用不知道/访问不到你的沙箱路径;
- SELinux 强制访问控制:即使知道路径,进程上下文不匹配 → EACCES。
2.2 私有目录访问规则
| 目录 | 用途 | 生命周期 | 是否备份 |
|---|---|---|---|
| files/ | 用户数据 | 随应用 | 是 |
| cache/ | 临时文件 | 可清理 | 否 |
| database/ | 数据库 | 随应用 | 是 |
| preferences/ | 配置 | 随应用 | 是 |
| share/ | 模块共享 | 随应用 | 是 |
工程铁律:可重建的数据放 cache/,用户数据放 files/。把缓存放 files/ 会导致空间膨胀;把用户数据放 cache/ 会导致系统清理后数据丢失。
2.3 公共媒体库授权模型
访问用户相册/音频/视频,鸿蒙提供两条路:
路径1: PhotoAccessHelper (受限访问)
→ 用户通过系统相册选择器授权特定照片
→ 返回 PhotoAsset 或 URI, 应用只能访问选中的项
→ 无需 READ_MEDIA 权限 (推荐)
路径2: 媒体权限 (全量访问)
→ ohos.permission.READ_IMAGEVIDEO (读图片视频)
→ ohos.permission.READ_AUDIO (读音频)
→ 用户授权后访问整个媒体库
→ 审核要求严格, 需在隐私声明中说明用途
2.4 URI 临时授权机制
文件通过 URI + 授权令牌跨应用传递,带时效、带模式、可撤销:
应用A (分享方):
1. 获取文件 URI: file://com.example.app/share/report.pdf
2. 签发临时授权: { uri, mode: 'read', expire: 30min }
3. 把 URI 传给应用B (通过 Want/剪贴板/分享面板)
应用B (接收方):
4. 收到 URI → 系统校验授权时效/模式
5. 有效 → 按 mode 访问
6. 过期/撤销 → 拒绝访问
三、源码/API 深度解析
3.1 私有目录路径获取
import { common } from '@kit.AbilityKit';
import { fileIo as fs } from '@kit.CoreFileKit';
function getPrivateDirs(context: common.UIAbilityContext): void {
const filesDir = context.filesDir; // files/
const cacheDir = context.cacheDir; // cache/
const tempDir = context.tempDir; // 临时目录
const databaseDir = context.databaseDir; // database/
// 写入 files/
const filePath = filesDir + '/user_data.txt';
fs.writeTextSync(filePath, '用户数据');
}
3.2 相册选择器(免权限方案)
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';
async function pickPhoto(context: common.UIAbilityContext): Promise<string | undefined> {
const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
const pickerOptions = new photoAccessHelper.PhotoSelectOptions();
pickerOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
pickerOptions.maxSelectNumber = 1;
const pickerResult = await phAccessHelper.select(pickerOptions);
return pickerResult.photoUris.length > 0 ? pickerResult.photoUris[0] : undefined;
}
3.3 媒体权限动态申请
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { common } from '@kit.AbilityKit';
async function requestMediaPermission(context: common.UIAbilityContext): Promise<boolean> {
const atManager = abilityAccessCtrl.createAtManager();
const permission: Permissions = 'ohos.permission.READ_IMAGEVIDEO';
try {
// 先检查
const status = await atManager.checkAccessToken(
context.applicationInfo.accessTokenId, permission);
if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
// 动态申请
const result = await atManager.requestPermissionsFromUser(context,
[permission]);
return result.authResults[0] === 0; // 0 = 授予
} catch (e) {
return false;
}
}
3.4 URI 临时授权
// 场景: 分享文件给其他应用
import { fileIo as fs } from '@kit.CoreFileKit';
async function shareFileWithUri(context: common.UIAbilityContext,
filePath: string, targetApp: string): Promise<string> {
// 1. 打开文件得到 fd
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
// 2. 生成 URI (开发态可通过文件路径构造 file:// 格式)
const uri = `file://${context.applicationInfo.bundleName}/share/${filePath.split('/').pop()}`;
// 3. 实际工程: 使用 Want 携带 URI + 权限 (flp 或 wantAgent)
fs.closeSync(file.fd);
return uri;
}
四、企业级实战落地
4.1 存储权限分级矩阵
| 数据 | 存储位置 | 权限方案 |
|---|---|---|
| 用户头像(用户选的) | files/avatar/ | 相册选择器(免权限) |
| 用户相册全量 | MediaLibrary | READ_IMAGEVIDEO(动态申请) |
| 导出的报表 | files/export/ + URI 分享 | URI 临时授权 |
| 下载文件 | files/download/ | 无需权限 |
| 缓存缩略图 | cache/thumb/ | 无需权限 |
4.2 媒体库访问双模式切换
// 策略: 优先用选择器(免权限), 需要全量时再申请权限
async function accessPhotos(context: common.UIAbilityContext,
needFullAccess: boolean): Promise<void> {
if (needFullAccess) {
const granted = await requestMediaPermission(context);
if (!granted) {
// 未授权 → 降级为选择器
await pickPhoto(context);
}
} else {
await pickPhoto(context); // 选择器模式, 零权限
}
}
4.3 授权生命周期管理
授权清单 (记录所有已获得的存储授权):
├─ 授权类型: 媒体权限 / URI临时 / 选择器单项
├─ 授权时间 / 到期时间
└─ 撤销时机: 任务完成即撤销
撤销动作:
- 权限: 提示用户去设置中关闭 (应用侧无法主动撤销系统权限)
- URI: 授权自然过期 / 主动 revoke
- 数据: 使用完后删除本地副本
4.4 合规自查清单
| 检查项 | 通过标准 |
|---|---|
| 权限最小化 | 不用存储权限的场景绝不申请 |
| 用途声明 | 隐私政策中说明每项存储权限用途 |
| 选择器优先 | 单张/单文件场景走系统选择器 |
| URI 有时效 | 分享授权全部带过期时间 |
| 数据不落盘 | 能流式处理的不保存文件 |
| 权限可撤销 | 用户可随时关闭权限且应用不崩溃 |
五、问题排查与性能优化
| 坑 | 现象 | 原因 | 解决 |
|---|---|---|---|
| 上架被拒 | 申请了 READ_MEDIA | 用途不明/可免权限 | 改用选择器 |
| 权限弹窗不出现 | request 无响应 | 未在 module.json5 声明 | 声明权限 |
| 访问相册报错 | 401/403 | 授权被拒未降级 | 降级选择器 |
| 分享文件打不开 | 目标应用无权 | URI 未授权 | 签发临时授权 |
| 数据丢失 | 缓存被清 | 放错目录 | 用户数据放 files/ |
| 空间膨胀 | files/ 无限增长 | 临时文件堆积 | cache/ + 定期清理 |
| 隐私违规 | 文件被其他应用读取 | 分享授权过期未回收 | 时效管控 |
5.1 权限拒绝后的优雅降级
权限被拒绝 ≠ 功能不可用
降级链:
全量媒体权限 → 系统选择器(单次授权) → 提示用户手动选择
文件权限 → FilePicker → 手动选择文件
分享权限 → 引导用户使用系统分享面板
5.2 存储空间监控
// 定期检查 files/ 目录大小, 超阈值告警
function checkStorage(context: common.UIAbilityContext): number {
let total = 0;
const walk = (dir: string) => {
const entries = fs.listFileSync(dir);
entries.forEach((name: string) => {
const p = `${dir}/${name}`;
const stat = fs.statSync(p);
if (stat.isDirectory()) { walk(p); } else { total += stat.size; }
});
};
walk(context.filesDir);
return total; // 字节
}
六、高阶总结与最佳实践
- 沙箱隔离是底线:应用私有目录默认隔离,SELinux 兜底,不需要也不应该放宽。
- 选择器优先于权限:能走系统选择器(相册/文件)的绝不动态申请全量权限——这是上架合规的第一原则。
- URI 授权带时效:跨应用共享一律临时授权,用后即失效。
- 目录纪律:files/ 存用户数据、cache/ 存可重建缓存,混淆二者必踩坑。
- 降级链:权限被拒后要有功能降级方案,而不是直接崩溃或失效。
一句话记住:私有目录免权限、媒体库选授权、跨应用临时授权、被拒后优雅降级——存储权限的本质是"最小授权 + 用户可控"。
更多推荐



所有评论(0)