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

一、前置思考

权限系统是应用安全的"门禁"。但传统移动平台的权限管控存在三个顽疾:

  1. 一次授权永久有效:用户授权后,应用何时用了权限、用了多少次,用户完全不知道
  2. 申请时机随意:应用启动时一口气申请十几个权限,用户被迫全量授权
  3. 权限与身份脱节:应用升级、被重打包后,原权限可能被滥用

这三个问题的本质是:权限缺乏"身份绑定"和"生命周期"。传统的权限模型是"应用申请 → 系统弹窗 → 用户点允许 → 永久有效",一旦授权就失去了管控能力。

鸿蒙的权限管控通过 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:

  1. 权限模型:展示 7 种权限的分级(system_grant/user_grant/system_basic)、申请模式(随装静默/运行时动态),支持点击"授权/拒绝"模拟动态申请,并记录审计日志
  2. Token令牌:展示 accessToken 的 tokenId/签发时间/过期时间/权限集,支持撤销 Token 演示"权限变更后 Token 失效"
  3. 使用审计:实时记录权限使用日志(谁、何时、什么权限、什么场景、结果)
// 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

六、总结

鸿蒙权限管控的设计哲学:

  1. 最小化:能静默(system_grant)就不弹窗,能延后(user_grant)就不提前
  2. 可审计:每次权限使用都可溯源、可查询
  3. 强绑定:权限与 tokenId(应用身份)绑定,身份变化权限即失效

落地建议:

  • 统一封装权限工具类(check/request/ensure/requestMultiple),全项目走一条链
  • user_grant 权限的 reason 文案要写清楚用途,降低用户拒绝率
  • 权限状态不要缓存,每次使用前实时校验
  • 拒绝处理要优雅:引导页 + 设置跳转,而不是反复弹窗
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐