鸿蒙权限管控高级原理:动态权限/静默权限/accessToken权限令牌/权限溯源/权限使用审计全链路机制



一、前置思考
权限系统是应用安全的"门禁"。但传统移动平台的权限管控存在三个顽疾:
- 一次授权永久有效:用户授权后,应用何时用了权限、用了多少次,用户完全不知道
- 申请时机随意:应用启动时一口气申请十几个权限,用户被迫全量授权
- 权限与身份脱节:应用升级、被重打包后,原权限可能被滥用
这三个问题的本质是:权限缺乏"身份绑定"和"生命周期"。传统的权限模型是"应用申请 → 系统弹窗 → 用户点允许 → 永久有效",一旦授权就失去了管控能力。
鸿蒙的权限管控通过 accessToken 权限令牌 机制把"身份 + 权限 + 时效 + 溯源"绑定在一起,实现了动态、静默、可审计的三级管控。权限不再是"一个开关",而是"一张带身份、带时效、可追溯的通行证"。
二、核心原理
2.1 accessToken 令牌结构
┌─────────────────────────────────────────┐
│ accessToken (AT) │
│ ┌──────────────────────────────────┐ │
│ │ tokenId 唯一标识 │ │
│ │ bundleName 应用包名 │ │
│ │ appId 应用标识 │ │
│ │ userId 用户ID │ │
│ │ deviceId 签发设备 │ │
│ │ permList 权限列表 │ │
│ │ expireTime 过期时间 │ │
│ │ signature 数字签名 │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
accessToken(简称 AT)是应用在系统内的"安全身份证"。它由系统在应用安装时签发,包含以下关键字段:
- tokenId:全局唯一标识,是权限校验的入参。每次
checkAccessTokenSync(tokenId, permission)都从 tokenId 出发定位应用身份。 - bundleName/appId/userId:三元组唯一定位"哪个用户在哪个设备上用哪个应用"。
- permList:当前已授予该应用的权限集合(含 system_grant 和已获准的 user_grant)。
- expireTime:令牌过期时间。过期后权限自动失效,需要重新申请。
- signature:系统对令牌内容的签名。任何篡改(比如往 permList 里加权限)都会导致签名校验失败。
为什么需要令牌而不是直接查应用权限表? 令牌把"应用身份"和"权限"绑定成一个不可分割的原子对象。应用被重打包(身份变化)、被降级、被撤销权限时,令牌要么失效要么重建——权限不会跟着"旧身份"继续存活。
2.2 权限分级模型
| 类型 | 申请方式 | 特点 | 示例 |
|---|---|---|---|
system_grant |
安装时静默授予 | 无弹窗,随装生效 | INTERNET、GET_NETWORK_INFO |
user_grant |
运行时动态申请 | 弹窗确认,可随时撤销 | CAMERA、MICROPHONE、LOCATION |
system_basic |
系统级 | 系统服务使用 | DISTRIBUTED_DATASYNC |
restricted |
受限 | 仅系统应用可申请 | 敏感能力 |
分级的核心思想是**“把选择权交给该交的人”**:
system_grant:不涉及隐私或风险极低,安装时直接授予,不打扰用户。但注意:即使静默授予,系统同样记录在案,使用仍可审计。user_grant:涉及隐私(相机、麦克风、定位、通讯录等),必须由用户在运行时显式确认。用户可以在设置中随时撤销,撤销后应用下次使用前必须重新申请。system_basic/restricted:系统级能力,普通应用申请会被拒绝。
2.3 权限校验链路
应用调用敏感 API
│
▼
AtManager.checkAccessTokenSync(tokenId, permission)
│
├── 1. 解析 tokenId → 定位应用身份
├── 2. 查询该应用权限表
├── 3. 校验权限是否在 permList 中
├── 4. 校验权限是否过期/被撤销
└── 5. 返回 GrantStatus(已授予/已拒绝)
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
export class PermissionAuditor {
private atManager: abilityAccessCtrl.AtManager;
constructor() {
this.atManager = abilityAccessCtrl.createAtManager();
}
// 同步检查(推荐高频调用场景)
checkSync(context: common.Context, permission: Permissions): boolean {
const tokenId: number = context.applicationInfo.accessTokenId;
const status: abilityAccessCtrl.GrantStatus =
this.atManager.checkAccessTokenSync(tokenId, permission);
return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
// 动态申请(user_grant 权限)
async requestFromUser(
context: common.UIAbilityContext,
permissions: Array<Permissions>
): Promise<Array<boolean>> {
const result = await this.atManager.requestPermissionsFromUser(context, permissions);
const results: Array<boolean> = [];
for (let i: number = 0; i < result.authResults.length; i++) {
results.push(
result.authResults[i] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
);
}
return results;
}
}
校验链路的关键点:权限校验是"实时"的,不是"缓存"的。每次调用敏感 API 都要走完整链路。这意味着:
- 用户刚在设置里撤销了相机权限,应用下一次调相机 API 立刻失败(不会等到重启)
- 应用无法通过缓存"已授权"状态来绕过撤销
- 权限被撤销后,应用必须在代码里处理"权限丢失"的运行时状态
2.4 权限溯源与使用审计
- 权限使用记录:系统记录每次权限被使用的场景、时间、频率
- 权限溯源:任何权限状态变更(授予/拒绝/撤销)都可追溯操作者与时间
- 透明化展示:用户可在设置中查看"哪些应用用了哪些权限、用了多少次"
鸿蒙把权限审计做成了系统级能力,而不是依赖应用自觉。即使应用自身不记录,系统侧也会记录权限使用情况,并展示在系统的"隐私管理"面板中。这对用户是透明保障,对应用则是合规压力——后台偷偷用权限的行为无处遁形。
三、典型问题场景
3.1 场景一:应用被重打包后权限被滥用
攻击者操作:
1. 反编译正版应用,植入恶意代码
2. 保留原签名(盗取证书)或诱导用户安装
3. 利用应用已获得的权限偷数据
被拦截的环节:
① 重打包后签名证书变化 → accessToken 签发时校验签名失败
② tokenId 与签名绑定,签名不一致 → 令牌失效,权限全部作废
③ 即使强制安装成功,原有 user_grant 权限需要重新申请
3.2 场景二:用户撤销权限后应用继续使用
攻击者操作(或用户操作):
1. 用户在某应用中授权了定位权限
2. 用户在系统设置中撤销该权限
3. 应用后台任务仍尝试获取位置
被拦截的环节:
① 后台任务调定位 API → checkAccessTokenSync 实时校验 → 返回拒绝
② 应用拿不到位置数据,只能拿到粗粒度信息或错误
③ 系统记录这次"权限被拒"的访问尝试,展示在审计面板
3.3 场景三:应用启动时批量申请权限
错误做法:
1. 应用启动 → 一口气申请相机/麦克风/定位/通讯录
2. 用户被迫全量授权(拒绝率极高)
3. 应用商店审核发现大量无关权限 → 下架风险
正确做法(见实战落地):
1. 权限跟随业务场景按需申请
2. 每个权限申请前说明用途(rationale)
3. 拒绝后引导用户,不反复弹窗
四、实战落地
对应 Demo 页面:entry/src/main/ets/pages/PermissionControlDemo.ets
Demo 包含三个 Tab:
- 权限模型:展示 7 种权限的分级(system_grant/user_grant/system_basic)、申请模式(随装静默/运行时动态),支持点击"授权/拒绝"模拟动态申请,并记录审计日志
- Token令牌:展示 accessToken 的 tokenId/签发时间/过期时间/权限集,支持撤销 Token 演示"权限变更后 Token 失效"
- 使用审计:实时记录权限使用日志(谁、何时、什么权限、什么场景、结果)
// Demo 核心:动态权限申请
private requestPerm(perm: PermItem): void {
const newPerms: PermItem[] = [];
for (let i: number = 0; i < this.perms.length; i++) {
if (this.perms[i].name === perm.name) {
newPerms.push({
name: perm.name, level: perm.level, mode: perm.mode,
status: 'granted', desc: perm.desc
});
} else {
newPerms.push(this.perms[i]);
}
}
this.perms = newPerms;
this.addAudit(perm.name, '手动触发', '✅ 已授权', '用户通过应用内按钮授权');
}
4.1 工程化落地:统一权限工具类
建议所有权限操作收敛到一个工具类,全项目走一条链:
export class PermissionUtil {
private static atManager: abilityAccessCtrl.AtManager =
abilityAccessCtrl.createAtManager();
// 检查权限(实时,不缓存)
static check(context: common.Context, perm: Permissions): boolean {
return this.atManager.checkAccessTokenSync(
context.applicationInfo.accessTokenId, perm
) === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
// 申请权限(带结果回调)
static async request(
context: common.UIAbilityContext, perm: Permissions
): Promise<boolean> {
const result = await this.atManager.requestPermissionsFromUser(context, [perm]);
return result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
// 确保权限:没有则申请,返回是否可用
static async ensure(
context: common.UIAbilityContext, perm: Permissions
): Promise<boolean> {
if (this.check(context, perm)) {
return true;
}
return this.request(context, perm);
}
}
统一封装的收益:
- 权限名、申请时机、拒绝处理集中管理,避免散落各处
- 可统一加埋点:每次权限申请/拒绝都记录审计日志
- 可统一处理"拒绝后跳设置"的引导逻辑
4.2 权限撤销的运行时处理
用户可能在应用运行中撤销权限,应用必须优雅降级:
// 监听权限变更(示意)
// 在 UIAbility 的 onForeground 或合适时机重新检查关键权限
private onAppForeground(): void {
// 关键权限丢失时提示并降级
if (!PermissionUtil.check(this.context, 'ohos.permission.CAMERA')) {
// 相机功能入口置灰/隐藏
this.cameraEnabled = false;
}
}
核心原则:权限状态永远以"实时校验"为准,绝不缓存。功能入口的状态要与权限状态联动,权限丢了功能就降级,而不是让用户点了功能才报错。
五、避坑速查
| 坑 | 现象 | 原因 | 解决 |
|---|---|---|---|
| user_grant 未声明 reason | 编译报错 | user_grant 必须提供 reason+usedScene | module.json5 补全字段 |
| 权限申请不弹窗 | 调了 request 无反应 | 在非 UIAbility 上下文调用 | 使用 UIAbilityContext |
| 用户拒绝后反复弹窗 | 体验差/被用户卸载 | 未处理拒绝状态 | 拒绝后进入引导页,提供设置跳转 |
| 撤销权限不生效 | 业务还在用权限 | 应用缓存了授权状态 | 每次使用前实时 checkAccessTokenSync |
| 权限名拼写错误 | 检查总返回 -1 | 权限常量不存在 | 对照文档核实权限名 |
| 升级后权限丢失 | 旧权限失效 | 权限表随版本变更 | 启动时统一 ensure 关键权限 |
| 后台任务用权限被拒 | 后台同步失败 | 后台使用 user_grant 权限受限 | 使用合规后台任务 + 前台时预取 |
| 多实例应用权限混乱 | 不同实例权限不同 | 未区分 instanceId | 按实例分别处理权限状态 |
| 权限申请时机过早 | 用户反感拒绝 | 启动时批量申请 | 跟随业务场景按需申请 |
| 忽略"仅一次"选项 | 用户每次都要授权 | 未适配"本次允许"模式 | 每次使用前重新 check |
六、总结
鸿蒙权限管控的设计哲学:
- 最小化:能静默(system_grant)就不弹窗,能延后(user_grant)就不提前
- 可审计:每次权限使用都可溯源、可查询
- 强绑定:权限与 tokenId(应用身份)绑定,身份变化权限即失效
落地建议:
- 统一封装权限工具类(check/request/ensure/requestMultiple),全项目走一条链
- user_grant 权限的 reason 文案要写清楚用途,降低用户拒绝率
- 权限状态不要缓存,每次使用前实时校验
- 拒绝处理要优雅:引导页 + 设置跳转,而不是反复弹窗
更多推荐

所有评论(0)