HarmonyOS APP《画伴梦工厂》开发第26篇:安全权限管理——abilityAccessCtrl 实战

第4.2篇:安全权限管理------abilityAccessCtrl 实战

难度 :⭐⭐ 进阶

前置知识 :第 4.1 篇 canIUse 系统能力检测

涉及源文件products/default/src/main/ets/services/PermissionGuard.etsproducts/default/src/main/ets/components/CreationComponents.ets



概述

HarmonyOS 的权限管理体系与 Android 和 iOS 既有相似之处,也有独特的设计理念。在"画伴梦工厂"中,我们需要使用相机拍照、调用麦克风录音、从相册选择图片------这些操作都涉及敏感权限的申请与管理。

本文将通过项目中的 PermissionGuard 服务,系统讲解 HarmonyOS 权限管理的核心 API abilityAccessCtrl,涵盖权限请求的异步流程、结果回调处理、敏感权限的申请策略以及 Scope 作用域访问的实践。


一、HarmonyOS 权限体系概览

1.1 权限分类

HarmonyOS 将权限分为三个层级:

权限等级 说明 示例 申请方式
system_basic(基础) 对系统基本资源的访问 ohos.permission.INTERNET 仅需 module.json5 声明
system_core(核心) 对用户敏感数据的访问 ohos.permission.CAMERAohos.permission.MICROPHONE 须动态弹窗授权
system_basic(受限) 存在隐私风险的普通权限 ohos.permission.READ_MEDIA 部分设备安装时拒绝

其中,CAMERAMICROPHONE 属于 system_core 级别 的敏感权限,必须通过 abilityAccessCtrl 的动态授权 API 在运行时弹窗询问用户。

1.2 权限申请流程

在 HarmonyOS 中,一个完整的权限申请流程包含三步:

  1. 静态声明 :在 module.json5 中声明所需权限
  2. 动态申请 :在运行时调用 requestPermissionsFromUser 弹窗询问
  3. 结果处理:根据用户的授权结果决定是否继续操作

只有第 1 步是编译期必需的,但实际项目中,核心敏感权限(如 CAMERA)应该在运行时动态申请,即使用户拒绝也不会导致应用崩溃。


二、PermissionGuard 服务封装

项目中将所有权限申请逻辑抽取为独立的 PermissionGuard 服务,放在 services/PermissionGuard.ets 中。这样做的好处是:权限逻辑与 UI 层解耦,便于维护和测试。

2.1 返回值类型定义

typescript 复制代码
export interface PermissionResult {
  granted: boolean;   // 是否获得授权
  message: string;    // 授权失败时的提示信息
}

PermissionResult 是权限申请的通用接口,调用方通过检查 granted 字段决定后续逻辑。

2.2 统一请求入口

typescript 复制代码
private static async request(context: common.UIAbilityContext, 
    permissions: Permissions[], deniedMessage: string): Promise<PermissionResult> {
  try {
    const manager = abilityAccessCtrl.createAtManager();
    const result = await manager.requestPermissionsFromUser(context, permissions);
    for (let i = 0; i < result.authResults.length; i++) {
      if (result.authResults[i] !== 0) {
        return { granted: false, message: deniedMessage };
      }
    }
    return { granted: true, message: '' };
  } catch (error) {
    return { granted: false, message: deniedMessage };
  }
}

三个方法的设计体现了清晰的职责分离:

  • request(私有)------通用的权限请求方法,接收权限列表和拒绝提示信息
  • requestCamera ------指定 CAMERA 权限和相机权限的提示文案
  • requestMicrophone ------指定 MICROPHONE 权限和麦克风权限的提示文案

三、abilityAccessCtrl.createAtManager() API 详解

abilityAccessCtrl 是 HarmonyOS 权限管理的核心模块,位于 @kit.AbilityKit 中。它提供了以下关键能力:

API 说明
createAtManager() 创建 AccessToken(权限令牌)管理器实例
requestPermissionsFromUser() 向用户弹窗请求权限,异步返回授权结果
verifyAccessToken() 校验指定权限是否已被授予(同步检查)
grantPermissions() 系统级授予权限(仅供系统应用使用)
revokePermissions() 系统级撤销权限(仅供系统应用使用)

在"画伴梦工厂"中,我们主要使用前两个 API:

typescript 复制代码
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';

const manager = abilityAccessCtrl.createAtManager();

设计要点

  • createAtManager() 是静态工厂方法,返回的是 AtManager 实例
  • AtManager 是单例模式------多次调用返回同一个实例
  • 每个应用进程只有一个 AtManager,因此可以在应用启动时创建一次并复用

四、requestPermissionsFromUser 异步流程

4.1 方法签名

typescript 复制代码
requestPermissionsFromUser(
  context: common.UIAbilityContext, 
  permissions: Permissions[]
): Promise<PermissionRequestResult>

参数说明:

参数 类型 说明
context common.UIAbilityContext 当前 UIAbility 的上下文,用于弹出系统授权对话框
permissions Permissions[] 要申请的权限数组,一次可申请多个权限

返回值 PermissionRequestResult 的结构:

typescript 复制代码
interface PermissionRequestResult {
  permissions: Permissions[];  // 申请的权限列表(与入参一致)
  authResults: number[];       // 每个权限的授权结果,0 = 授权,-1 = 拒绝
}

4.2 异步调用模式

方法返回 Promise,因此可以使用 async/await 语法进行流程控制:

typescript 复制代码
const result = await manager.requestPermissionsFromUser(context, permissions);

这行代码会:

  1. 弹出系统的权限请求对话框
  2. 阻塞等待用户做出选择(授权或拒绝)
  3. 用户操作完成后,Promise resolve 返回授权结果

4.3 context 参数获取

在 UI 组件中,context 通过 getContext(this) 获取:

typescript 复制代码
const context = getContext(this) as common.UIAbilityContext;

getContext(this) 是 ArkUI 提供的内置函数,返回当前组件的上下文对象。需要强制转换为 common.UIAbilityContext 类型,因为 requestPermissionsFromUser 要求 UIAbilityContext 类型的参数。


五、PermissionResult 回调结果处理

5.1 authResults 数组语义

result.authResults 是与 permissions 入参一一对应的整型数组:

含义
0 授权成功(PERMISSION_GRANTED)
-1 拒绝授权(PERMISSION_DENIED)
-2 未找到该权限(检查 module.json5 声明)
-3 标记为不再询问(用户勾选了"不再询问")

5.2 遍历检查策略

在 PermissionGuard 中,使用遍历方式逐个检查结果:

typescript 复制代码
for (let i = 0; i < result.authResults.length; i++) {
  if (result.authResults[i] !== 0) {
    return { granted: false, message: deniedMessage };
  }
}

采用 "任意一个拒绝则整体拒绝" 策略------只要有一个权限被拒绝,就返回 granted: false。这在申请单个权限时用循环看似多余,但为未来"一次申请多个权限"的场景预留了扩展性。

5.3 异常兜底

typescript 复制代码
catch (error) {
  return { granted: false, message: deniedMessage };
}

如果 requestPermissionsFromUser 本身抛出异常(例如 context 无效、权限列表为空等),catch 块同样返回 granted: false。这样调用方只需要判断 granted 一个字段,无需关心内部是"用户拒绝"还是"系统异常"。


六、敏感权限申请策略(CAMERA / MICROPHONE)

6.1 CAMERA 权限

相机权限是项目中最重要的敏感权限。其申请流程在 requestCamera 中封装:

typescript 复制代码
static async requestCamera(context: common.UIAbilityContext): Promise<PermissionResult> {
  return PermissionGuard.request(context, ['ohos.permission.CAMERA'],
    '请在设置里打开相机权限后继续');
}

当用户在 CreationComponents 中点击"拍照采集"按钮时,触发完整的权限检查链路:

typescript 复制代码
private async openCamera() {
  const context = getContext(this) as common.UIAbilityContext;
  const permissionResult: PermissionResult = await PermissionGuard.requestCamera(context);
  if (!permissionResult.granted) {
    this.noticeText = permissionResult.message;
    return;
  }
  // 继续打开系统相机
  context.startAbilityForResult({
    action: 'ohos.want.action.imageCapture'
  }).then(/* ... */);
}

6.2 MICROPHONE 权限

麦克风权限的申请模式与相机完全一致:

typescript 复制代码
static async requestMicrophone(context: common.UIAbilityContext): Promise<PermissionResult> {
  return PermissionGuard.request(context, ['ohos.permission.MICROPHONE'],
    '请在设置里打开麦克风权限后继续');
}

6.3 敏感权限申请的最佳实践

原则 说明
最小化 只申请当前功能必需的权限,不提前申请未使用的权限
场景化 在用户即将使用该功能时才申请,而非应用启动时
可解释 拒绝后给出明确的引导提示,告知用户为何需要该权限
降级处理 权限被拒后,功能应优雅降级而非直接崩溃

项目完全遵循了这些原则------requestCamera 只在用户点击"拍照采集"按钮时才调用,而不是在 aboutToAppear 中提前申请。拒绝后通过 noticeText 告知用户如何打开权限。


七、PhotoViewPicker 的 Scope 作用域访问模式

7.1 相册选择的权限特殊性

值得注意的是,PermissionGuard.requestAlbum 的实现非常特殊:

typescript 复制代码
static async requestAlbum(context: common.UIAbilityContext): Promise<PermissionResult> {
  // PhotoViewPicker grants scoped access to the selected media item.
  // Do not declare broad media-read permissions here; 
  // some devices reject them at install time.
  return { granted: true, message: '' };
}

直接返回授权成功,不申请任何权限 。这是因为 PhotoViewPicker 采用了 Scope 作用域访问模式。

7.2 Scope 访问模式

传统权限模型(如 Android)中,访问相册需要声明 READ_MEDIA_IMAGES 权限,这意味着应用可以读取用户相册中所有图片。而 HarmonyOS 的 PhotoViewPicker 打破了这种"全有或全无"的模式:

复制代码
传统权限模型:
  声明 READ_MEDIA → 用户授权 → 应用可读取整个媒体库

Scope 访问模型(PhotoViewPicker):
  启动 Picker → 用户选择 → 应用仅可访问被选中的文件

这种设计的好处:

  • 隐私保护:应用只能访问用户明确选中的文件
  • 权限简化:开发者无需处理繁重的媒体库权限
  • 安装兼容 :部分设备会在安装时拒绝 READ_MEDIA 声明,Scope 模式避免了这个问题

7.3 何时需要 READ_MEDIA

如果应用需要通过文件路径直接访问媒体文件(而非通过 Picker 选择),则需要声明 ohos.permission.READ_MEDIA。但在"画伴梦工厂"中,所有相册选择都通过 PhotoViewPicker 完成,因此完全不需要该权限。


八、权限拒绝后的用户引导

8.1 noticeText 显示机制

当权限被拒绝时,项目通过 noticeText 向用户展示提示信息:

typescript 复制代码
// PermissionGuard 返回的 deniedMessage 直接被赋给 noticeText
this.noticeText = permissionResult.message;  // 如 "请在设置里打开相机权限后继续"

noticeText@State 变量,在 UI 中绑定到通知栏组件:

typescript 复制代码
// 假设的 NoticeBar 结构
Text(this.noticeText)
  .fontColor('#D94C3D')       // 红色警示
  .fontSize(14)
  .visibility(this.noticeText !== '' ? Visibility.Visible : Visibility.Hidden)

8.2 引导逻辑

当用户首次拒绝权限后,后续再次触发同功能时,系统弹窗可能不再出现(用户勾选了"不再询问")。此时需要引导用户前往系统设置中手动开启。

目前的 PermissionGuard 将所有拒绝场景统一输出 deniedMessage。更完善的方案可以增加对 authResults[i] === -3(不再询问)的区分处理:

typescript 复制代码
if (result.authResults[i] === -3) {
  return { 
    granted: false, 
    message: '已拒绝权限并不再询问,请前往「设置 > 应用 > 画伴梦工厂 > 权限」中手动开启' 
  };
}

8.3 完整时序

复制代码
用户点击"拍照采集"
    │
    ▼
PermissionGuard.requestCamera()
    │
    ├── 用户授权
    │     └── granted: true → 启动系统相机
    │
    ├── 用户拒绝(单次)
    │     └── granted: false → noticeText = "请在设置里打开相机权限后继续"
    │
    └── 用户拒绝(不再询问)
          └── granted: false → noticeText = "已拒绝并不再询问,请前往设置中手动开启"

九、完整代码与单元职责

9.1 PermissionGuard.ets 完整源码

typescript 复制代码
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';

export interface PermissionResult {
  granted: boolean;
  message: string;
}

export class PermissionGuard {
  static async requestCamera(context: common.UIAbilityContext): Promise<PermissionResult> {
    return PermissionGuard.request(context, ['ohos.permission.CAMERA'],
      '请在设置里打开相机权限后继续');
  }

  static async requestAlbum(context: common.UIAbilityContext): Promise<PermissionResult> {
    return { granted: true, message: '' };
  }

  static async requestMicrophone(context: common.UIAbilityContext): Promise<PermissionResult> {
    return PermissionGuard.request(context, ['ohos.permission.MICROPHONE'],
      '请在设置里打开麦克风权限后继续');
  }

  private static async request(context: common.UIAbilityContext, 
      permissions: Permissions[], deniedMessage: string): Promise<PermissionResult> {
    try {
      const manager = abilityAccessCtrl.createAtManager();
      const result = await manager.requestPermissionsFromUser(context, permissions);
      for (let i = 0; i < result.authResults.length; i++) {
        if (result.authResults[i] !== 0) {
          return { granted: false, message: deniedMessage };
        }
      }
      return { granted: true, message: '' };
    } catch (error) {
      return { granted: false, message: deniedMessage };
    }
  }
}

9.2 各方法职责总览

方法 类型 权限 返回 deniedMessage 说明
requestCamera public static ohos.permission.CAMERA "请在设置里打开相机权限后继续" 调用系统相机前使用
requestAlbum public static 无(Scope 模式) ""(空字符串) PhotoViewPicker 无需额外权限
requestMicrophone public static ohos.permission.MICROPHONE "请在设置里打开麦克风权限后继续" 需要录音功能时使用
request private static 任意 Permissions[] 由上层传入 统一的权限请求实现

十、最佳实践总结

10.1 权限管理架构建议

  1. 抽取独立服务 :将权限逻辑从 UI 组件中分离到独立服务(如 PermissionGuard),便于维护和复用
  2. 统一返回值 :定义统一的 PermissionResult 接口,降低调用方的复杂度
  3. 异常安全 :使用 try-catch 包裹 requestPermissionsFromUser,防止异常导致 UI 卡死
  4. 差异化提示 :根据 authResults[i] 的值给出不同的提示文案

10.2 敏感权限设计原则

  • 场景触发:在用户即将使用功能时申请,而非应用启动时预申请
  • 一次一个:尽量一次只申请一个敏感权限,避免多个弹窗连续出现
  • 降级路径:权限被拒后提供替代方案(如示例画作),而不是直接阻断流程
  • Scope 优先 :优先使用 PhotoViewPicker 等 Scope 访问 API,减少敏感权限依赖

10.3 常见陷阱

陷阱 解决方案
忘记在 module.json5 中声明权限 module.json5requestPermissions 数组中添加权限声明
context 类型不匹配 使用 getContext(this) as common.UIAbilityContext 进行类型转换
一次申请过多权限 按功能模块拆分,只在需要时申请对应权限
忽略"不再询问"状态 检测 authResults[i] === -3,引导用户前往设置手动开启

总结

本文通过"画伴梦工厂"的 PermissionGuard 服务,系统梳理了 HarmonyOS 权限管理的核心知识点:

知识点 实现方式
权限管理入口 abilityAccessCtrl.createAtManager()
动态权限请求 manager.requestPermissionsFromUser(context, permissions)
异步模式 async/await + Promise
结果判断 遍历 authResults0 为授权,非 0 为拒绝
敏感权限策略 场景触发、最小化申请、优雅降级
Scope 访问 PhotoViewPicker 无需 READ_MEDIA 权限
拒绝引导 通过 noticeText 展示提示信息

下一节我们将探讨 HarmonyOS 的隐私合规与数据保护,了解如何构建更安全的用户数据访问机制。


参考源码

本文所有代码均来自项目文件:

  • products/default/src/main/ets/services/PermissionGuard.ets --- 权限管理服务,封装 abilityAccessCtrl 核心操作
  • products/default/src/main/ets/components/CreationComponents.ets --- 创作组件,演示权限申请与 UI 的集成