鸿蒙百篇系列第 63 篇 · 专家
第 12 篇讲过权限管理,那是从"权限申请"的角度。这一篇我们要从"安全模型"的角度,看清楚鸿蒙是怎么做到"应用之间互不干扰"。这一篇不绕弯子:鸿蒙的安全机制是从内核层开始就搭好的,应用进程运行在三重隔离里——进程隔离、目录隔离、能力隔离。普通开发者感知不到,但一旦你要做"插件宿主"、“脚本沙箱”、"远程加载 HAP"这些场景,就必须把这层搞清楚。
一、可信基:最底层的硬件与内核
鸿蒙微内核的安全责任不是"应用层的功能",而是"这件事做不了"。它的几个关键能力:
进程隔离:每个应用有独立 PID,进程虚拟地址空间不能跨界访问
AccessToken:任何 API 调用都隐式带一个"调用者身份",由内核+系统服务共同校验
签名校验:HAP/APP 包在安装时校验签名链,未签名或被改动的包直接拒装
能力分类:把系统能力按
system_grant(安装时授予)、user_grant(运行时询问)区分
这一层对开发者来说"基本不用关心",但你写的代码运行的每一行都在这套约束之下。理解它的存在比理解它的实现更重要。
二、应用沙箱:三层"看不见"
应用沙箱是开发者最有体感的一层。每一个 HAP 包安装后,都有自己的:
独立文件系统目录:其他应用读不到
独立进程空间:不能读写其他应用内存
独立能力令牌:跨进程通信被强制鉴权
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)描述应用的"权限级别",以及"能申请多高级的权限":
权限本身也按等级分级。例如:
normal权限示例:ohos.permission.INTERNET、ohos.permission.GET_NETWORK_INFOsystem_basic权限示例:ohos.permission.READ_CONTACTS、ohos.permission.WRITE_CALENDARsystem_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或外部存储共享文件硬编码路径。
/sdcard/xxx这种路径跨设备不可靠,要用 picker 或 FileShare API把临时文件放
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 + 拉新促活是更稳的做法。
八、几个高频安全痛点
Token 不要写本地。
accessToken一律后端校验,本地只放短期 refreshToken,且用 KeyStore 而非明文文件截图与剪贴板做去敏。支付完成页的"完整卡号"应该显式隐藏,截图开关会被系统记录日志
登录态有生命周期。前后台切换时考虑是否要二次校验,比如支付应用要求"3分钟内再次支付需要指纹"
WebView 跨域隔离。第 55 篇的 Bridge 设计里讲过,JSContext 必须有 origin 白名单
不要把 APK/IPA 路径写在代码里。HAP/APP 路径安装时会变
九、和前几篇的衔接
第 12 篇讲权限申请流程,API 用了
requestPermissionsFromUser。本篇是从系统调用链解释为什么这步必要第 33 篇讲安全加固,聚焦混淆与签名。本篇偏向系统级隔离
第 56 篇讲 IPC 与 DataShareExtension,是沙箱间受控通信的标准方式
十、小结
鸿蒙的安全沙箱是"四层思维":可信基 → 系统服务 → 应用沙箱 → 开发者策略。每一层都在做"隔离 + 鉴权"。开发者要做的不是绕过它,而是在它的边界上设计清晰的能力面(API),让权限检查有明确错处可追。
下一篇进入 NDK 原生开发,看怎么用 C++ 写高性能模块,再跟 ArkTS 拼起来。