Administrator
发布于 2026-09-03 / 6,633 阅读
0
0

鸿蒙安全沙箱:进程隔离、目录边界与权限校验链

鸿蒙百篇系列第 63 篇 · 专家

第 12 篇讲过权限管理,那是从"权限申请"的角度。这一篇我们要从"安全模型"的角度,看清楚鸿蒙是怎么做到"应用之间互不干扰"。这一篇不绕弯子:鸿蒙的安全机制是从内核层开始就搭好的,应用进程运行在三重隔离里——进程隔离、目录隔离、能力隔离。普通开发者感知不到,但一旦你要做"插件宿主"、“脚本沙箱”、"远程加载 HAP"这些场景,就必须把这层搞清楚。

一、可信基:最底层的硬件与内核

鸿蒙微内核的安全责任不是"应用层的功能",而是"这件事做不了"。它的几个关键能力:

  • 进程隔离:每个应用有独立 PID,进程虚拟地址空间不能跨界访问

  • AccessToken:任何 API 调用都隐式带一个"调用者身份",由内核+系统服务共同校验

  • 签名校验:HAP/APP 包在安装时校验签名链,未签名或被改动的包直接拒装

  • 能力分类:把系统能力按 system_grant(安装时授予)、user_grant(运行时询问)区分

这一层对开发者来说"基本不用关心",但你写的代码运行的每一行都在这套约束之下。理解它的存在比理解它的实现更重要。

二、应用沙箱:三层"看不见"

应用沙箱是开发者最有体感的一层。每一个 HAP 包安装后,都有自己的:

  1. 独立文件系统目录:其他应用读不到

  2. 独立进程空间:不能读写其他应用内存

  3. 独立能力令牌:跨进程通信被强制鉴权

abilityStage 启动时通过 Context 拿到的目录就是沙箱内的入口:

const context = getContext(this);

// 这些路径都指向应用沙箱内
const filesDir = context.filesDir;          // files
const cacheDir = context.cacheDir;          // cache
const databaseDir = context.databaseDir;    // database
const preferencesDir = context.preferencesDir; // preferences

// 跨沙箱访问只能走受控接口
// 1) 系统选择器(picker)
// 2) 分享 API
// 3) DataShare / DataShareHelper
// 4) 分布式软总线的可信通道

// 错误示范:直接读另一个应用的文件目录
// fs.readFileSync('/data/data/other.app/files/secret.json') // ❌ 直接拒绝

很多团队从 Android 过来,第一反应就是"能不能像 Android 那样用 root 权限读别人文件"。答案是不能:鸿蒙的沙箱没有 root,因为没有"超级用户"这个概念。

三、APL 与权限等级

鸿蒙用 APL(Ability Privilege Level)描述应用的"权限级别",以及"能申请多高级的权限":

应用 APL

可申请权限等级

典型场景

normal

normal

普通第三方应用

system_basic

normal + system_basic

厂商预装应用

system_core

全部,包括 system_core

仅系统应用

权限本身也按等级分级。例如:

  • normal 权限示例:ohos.permission.INTERNET、ohos.permission.GET_NETWORK_INFO

  • system_basic 权限示例:ohos.permission.READ_CONTACTS、ohos.permission.WRITE_CALENDAR

  • system_core 权限示例:直接管理其他进程、修改全局策略

申请 ACL(Access Control List)的方式不在 module.json5 里写,而是单独向 AGC 申请证书。这跟 Android 上"申请更危险权限"是一个意思,只是流程更严格,文档更完整。

四、典型安全架构:调用链上的鉴权

当应用调一行 geolocation.getCurrentLocation(),背后会经历这么长的调用链:

应用代码 (ArkTS)
    ↓
geolocation JS API
    ↓
Geolocation Service (system server)
    ↓
AccessToken 检查 (ohos.permission.LOCATION)
    ↓
system_grant 检查 (已是系统授予?)
或 user_grant 检查 (用户是否弹窗同意?)
    ↓
LocationProvider (HAL 层)
    ↓
GNSS / Network Provider

每一环失败都会抛 BusinessError。这就是为什么你看到一个权限被拒时,错误信息总要看到 code 和 message——它们对应这棵树的不同分支。

来看一段带权限检查的代码片段:

import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import { geoLocationManager } from '@ohos.geoLocationManager';
import { BusinessError } from '@ohos.base';

async function getLocationSafely() {
  const atManager = abilityAccessCtrl.createAtManager();
  const tokenId = atManager.getTokenIdFromLocal().tokenId;

  // 1. 先检查
  const grant = await atManager.checkAccessToken(tokenId, 'ohos.permission.LOCATION');

  if (grant !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    // 2. 没授权则请求
    const requestResult = await atManager.requestPermissionsFromUser(
      getContext(this),
      ['ohos.permission.LOCATION']
    );

    if (requestResult.authResults[0] !== 0) {
      throw new Error('用户拒绝了定位权限');
    }
  }

  // 3. 已授权,调 API
  try {
    const location = await geoLocationManager.getCurrentLocation();
    return location;
  } catch (e) {
    const err = e as BusinessError;
    console.error(`定位失败: code=${err.code}, msg=${err.message}`);
    return null;
  }
}

checkAccessToken 不需要弹窗就告诉你"有没有",requestPermissionsFromUser 是真去弹窗请求。两者结合是请求用户敏感权限的标准模式。

五、文件域细节:哪个目录放什么

应用沙箱里的目录不是"随便写"的,鸿蒙对它们有清理策略:

目录

用途

清理策略

filesDir

用户主动保存的数据

卸载时清

cacheDir

缓存文件

系统空间不足时清

databaseDir

RDB 数据库

卸载时清

preferencesDir

Preferences 配置

卸载时清

tempDir

临时文件

进程退出清(API 12+)

实战中常见的几个错误:

  1. 把下载的文件放 filesDir。文件很大、可能被系统清理、能被备份到云端,应该用 cacheDir 或外部存储

  2. 共享文件硬编码路径。/sdcard/xxx 这种路径跨设备不可靠,要用 picker 或 FileShare API

  3. 把临时文件放 filesDir。结果是卸载前都不会被清理

正确的写法示例:

// 网络图片缓存:用 cacheDir
async function cacheImage(url: string): Promise<string> {
  const ctx = getContext(this);
  const tmpFile = ctx.cacheDir + '/img_' + Date.now() + '.png';
  // ... 下载到 tmpFile
  return tmpFile;
}

// 用户主动保存的文档:用 filesDir
async function saveDocument(content: string): Promise<string> {
  const ctx = getContext(this);
  const path = ctx.filesDir + '/note_' + Date.now() + '.txt';
  // ... 写入
  return path;
}

// 暴露给其他应用:用 fileShare API 而不是传路径
async function shareFile(path: string) {
  const ctx = getContext(this);
  // system Share Kit,会以 picker 让用户选择目标应用
  await ctx.startAbility({
    action: 'ohos.want.action.SEND_DATA',
    parameters: { 'ohos.extra.param.key.data': path }
  });
}

六、HiAppEvent:安全事件的审计与上报

鸿蒙提供了一个统一的"安全事件"接入点,叫 HiAppEvent,跟 Android 的 SafetyNet 思路类似。开发者可以订阅几种事件:

  • hiappevent.event.APP_CRASH(崩溃)

  • hiappevent.event.APP_FREEZE(卡死)

  • hiappevent.event.RESOURCE_OVERLIMIT(资源超限)

  • hiappevent.event.SECURITY_SIGN_VERIFY_FAILED(签名校验失败)

  • hiappevent.event.CALL_PERMISSION_GRANT_RESULT(权限请求结果)

订阅方式:

import hiAppEvent from '@ohos.hiappevent';

hiAppEvent.addWatcher({
  name: 'SecurityWatcher',
  eventTypes: [
    'hiappevent.event.APP_CRASH',
    'hiappevent.event.SECURITY_SIGN_VERIFY_FAILED',
    'hiappevent.event.CALL_PERMISSION_GRANT_RESULT'
  ],
  onTrigger: (cur, before) => {
    console.info(`事件: ${JSON.stringify(cur)}`);
    // 这里把事件上报到业务端,方便分析
  }
});

安全事件不只是写日志,关键是"决策":连续三次权限请求失败是用户在拒绝你;签名校验失败可能意味着包被攻击;资源超限可能意味着正在被恶意代码消耗。

七、动态加载的"沙箱加固"

如果你的应用支持"热更新"、“动态加载插件”,这里沙箱要被显式收紧。鸿蒙的标准做法是用 HSP(动态共享包)而不是任意 ZIP:

// 加载合法的 HSP
import { bundleManager } from '@kit.AbilityKit';

const info = await bundleManager.getBundleInfo({
  bundleName: 'com.example.plugin',
  bundleFlag: bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE
});

// HSP 必须经过签名校验才允许加载
if (info.signatureInfo && info.signatureInfo.appId) {
  // 可以加载
  console.info('插件可信,可执行');
}

如果是远程下发的"代码"想直接跑,请先放弃:当前 HarmonyOS NEXT 不允许未签名动态代码执行。改成 HSP + 拉新促活是更稳的做法。

八、几个高频安全痛点

  1. Token 不要写本地。accessToken 一律后端校验,本地只放短期 refreshToken,且用 KeyStore 而非明文文件

  2. 截图与剪贴板做去敏。支付完成页的"完整卡号"应该显式隐藏,截图开关会被系统记录日志

  3. 登录态有生命周期。前后台切换时考虑是否要二次校验,比如支付应用要求"3分钟内再次支付需要指纹"

  4. WebView 跨域隔离。第 55 篇的 Bridge 设计里讲过,JSContext 必须有 origin 白名单

  5. 不要把 APK/IPA 路径写在代码里。HAP/APP 路径安装时会变

九、和前几篇的衔接

  • 第 12 篇讲权限申请流程,API 用了 requestPermissionsFromUser。本篇是从系统调用链解释为什么这步必要

  • 第 33 篇讲安全加固,聚焦混淆与签名。本篇偏向系统级隔离

  • 第 56 篇讲 IPC 与 DataShareExtension,是沙箱间受控通信的标准方式

十、小结

鸿蒙的安全沙箱是"四层思维":可信基 → 系统服务 → 应用沙箱 → 开发者策略。每一层都在做"隔离 + 鉴权"。开发者要做的不是绕过它,而是在它的边界上设计清晰的能力面(API),让权限检查有明确错处可追。

下一篇进入 NDK 原生开发,看怎么用 C++ 写高性能模块,再跟 ArkTS 拼起来。


评论