在这里插入图片描述
在这里插入图片描述

一、前置思考

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;   // 字节
}

六、高阶总结与最佳实践

  1. 沙箱隔离是底线:应用私有目录默认隔离,SELinux 兜底,不需要也不应该放宽。
  2. 选择器优先于权限:能走系统选择器(相册/文件)的绝不动态申请全量权限——这是上架合规的第一原则。
  3. URI 授权带时效:跨应用共享一律临时授权,用后即失效。
  4. 目录纪律:files/ 存用户数据、cache/ 存可重建缓存,混淆二者必踩坑。
  5. 降级链:权限被拒后要有功能降级方案,而不是直接崩溃或失效。

一句话记住:私有目录免权限、媒体库选授权、跨应用临时授权、被拒后优雅降级——存储权限的本质是"最小授权 + 用户可控"。

Logo

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

更多推荐