前言

在移动应用开发的蛮荒时代,获取用户隐私就像是自助餐一样随意,应用安装时列出一长串清单,用户只要想用就必须全盘接受。但随着隐私安全成为操作系统的底线,这种“一揽子买卖”早已成为历史。

在鸿蒙 HarmonyOS 6 (API 20) 中,安全机制被提升到了前所未有的高度。系统引入了 ATM (Access Token Manager) 机制,构建了一座森严的“权限金字塔”。

对于开发者而言,这意味着我们不能再理所当然地调用相机、读取通讯录或者获取定位。每一个敏感操作之前,都必须经过一道严格的安检。

很多刚上手的开发者会被 module.json5 里的配置搞得晕头转向,或者在处理动态权限申请的回调地狱中苦不堪言。特别是当我们面对 system_basic 这种系统级权限时,往往会因为签名问题碰壁。

今天,我们就来彻底拆解鸿蒙的权限体系,并亲手封装一个 Promise 化的权限请求工具,让繁琐的授权流程变得像调用普通函数一样简单。

一、 权限金字塔:分级与 ATM 模型

鸿蒙的权限管理核心是 ATM 模型,它将权限划分为了不同的等级,形成了一个金字塔结构。位于塔底的是 system_grant(系统授权) 类型的权限,比如访问网络(ohos.permission.INTERNET)。这类权限只要我们在配置文件里声明了,应用安装时系统就会自动授予,不需要用户感知,属于“默认通行证”。

真正的挑战在于塔尖的 user_grant(用户授权) 类型。这包括相机、麦克风、位置、日历等涉及用户核心隐私的权限。对于这类权限,单纯的静态声明是不够的,我们必须在代码运行时,显式地向用户发起申请,系统会弹出一个标准化的授权弹窗,只有用户点击“允许”,我们才能拿到通行令牌(Token)。

此外,还有一个让很多开发者困惑的概念是 APL (Available Privilege Level),也就是应用权限等级。鸿蒙将应用分为 normalsystem_basicsystem_core 三个等级。普通的第三方应用默认是 normal 等级。如果你想申请一些高级权限(比如拦截骚扰电话、管理系统窗口),这些权限往往被标记为 system_basic。对于普通开发者来说,这意味着即便你声明了这些权限,系统也不会给你。

除非你的应用通过了特殊的签名证书配置(ACL)或者你是系统预置应用,否则这些高级权限就是禁区。所以,在设计功能时,务必先查阅文档,确认你所需的权限是否对 normal 等级开放。

二、 静态声明:module.json5 的门票

一切权限申请的起点都是 module.json5 文件。这里是我们向系统递交的“申请书”。在 requestPermissions 字段中,我们需要列出应用所需的所有权限。

这里有一个极易踩坑的细节:reason(申请理由)。对于 user_grant 类型的权限,reason 字段是必填的,而且它必须是一个 资源引用($r),不能是硬编码的字符串。系统在弹出授权框时,会展示这个 reason 里的文案,告诉用户你为什么要用相机。如果你的理由写得含糊不清,比如“需要使用相机”,用户很可能会拒绝;而如果你写“需要使用相机扫描二维码进行支付”,通过率就会大大提高。此外,usedScene 字段虽然是可选的,但建议填写,它可以声明权限使用的场景(比如是前台使用还是后台使用),这对于通过应用市场的审核至关重要。

      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:mic_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },

三、 动态申请:告别回调地狱

在完成了静态声明后,重头戏来了:动态申请。原生的 abilityAccessCtrl API 虽然功能强大,但调用起来略显繁琐。我们需要先获取 AtManager 实例,然后构建请求数组,最后调用 requestPermissionsFromUser。这个方法是异步的,原本是基于回调的设计,如果在业务逻辑中层层嵌套,代码的可读性会非常差。

我们需要的是一个 Promise 化 的封装。想象一下,我们希望在点击按钮时,只需要写一行 await PermissionManager.request('ohos.permission.CAMERA'),如果用户同意就继续执行,拒绝就抛出异常或提示。这样的代码才符合现代异步编程的审美。

为了实现这个目标,我们需要封装一个单例的 PermissionManager。在这个类中,我们不仅要处理申请逻辑,还要处理 权限校验 逻辑。因为用户可能会在设置中心手动关闭权限,所以每次使用功能前,我们都应该先通过 checkAccessToken 检查当前的授权状态。如果已经是 PERMISSION_GRANTED,就直接放行;如果是 PERMISSION_DENIED,才发起弹窗申请。

四、 优雅的二次确认与引导

还有一个用户体验的痛点:如果用户点击了“禁止”并且勾选了“不再提示”,系统自带的弹窗就再也不会出现了。这时候如果不做处理,用户点击按钮会没有任何反应,以为程序坏了。

一个成熟的权限工具类,应该具备 二次引导 的能力。当 requestPermissionsFromUser 返回的结果显示用户拒绝了授权,我们需要弹出一个自定义的 Dialog,委婉地告诉用户:“我们需要这个权限才能拍照,请去设置中心开启。”并提供一个按钮,直接跳转到系统的应用设置页。这才是完整的闭环。

下面是一个完整的权限管理工具类封装。它包含了检查权限、申请权限以及处理用户拒绝后的引导逻辑。你可以直接将这个 PermissionManager 复制到你的项目中,用它来替换掉那些散落在各处的原生 API 调用。

import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

// -------------------------------------------------------------
// 1. 权限管理单例封装
// -------------------------------------------------------------
class PermissionManager {
  private static instance: PermissionManager;
  private atManager: abilityAccessCtrl.AtManager;

  private constructor() {
    this.atManager = abilityAccessCtrl.createAtManager();
  }

  public static getInstance(): PermissionManager {
    if (!PermissionManager.instance) {
      PermissionManager.instance = new PermissionManager();
    }
    return PermissionManager.instance;
  }

  /**
   * 检查是否已授予权限
   * @param permission 权限名称
   */
  async checkPermission(permission: Permissions): Promise<boolean> {
    try {
      const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
      const appInfo = bundleInfo.appInfo;
      const tokenId = appInfo.accessTokenId;

      const grantStatus = await this.atManager.checkAccessToken(tokenId, permission);
      return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
    } catch (error) {
      console.error('Check permission failed:', error);
      return false;
    }
  }

  /**
   * 核心方法:请求权限 (Promise 化)
   * @param context UIAbilityContext
   * @param permissions 权限数组
   * @returns boolean 是否全部授权成功
   */
  async requestPermissions(context: common.UIAbilityContext, permissions: Permissions[]): Promise<boolean> {
    try {
      // 1. 先发起系统弹窗请求
      const result = await this.atManager.requestPermissionsFromUser(context, permissions);
      
      // 2. 检查请求结果
      // result.authResults 数组对应 permissions 数组的授权状态
      const isAllGranted = result.authResults.every(status => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);

      if (isAllGranted) {
        return true;
      } else {
        // 3. 如果被拒绝,可以在这里统一处理引导逻辑
        // 比如检测到用户勾选了"不再提示",则弹出自定义弹窗引导去设置页
        // 这里为了简化,只返回 false
        return false;
      }
    } catch (error) {
      const err = error as BusinessError;
      console.error(`Request permissions failed, code: ${err.code}, message: ${err.message}`);
      return false;
    }
  }

  /**
   * 引导用户去设置页开启权限
   * 当用户勾选"不再提示"导致无法弹出授权框时调用
   */
  openSystemSettings(context: common.UIAbilityContext) {
    const want: common.Want = {
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri: 'application_info_entry', // 跳转到应用详情页
      parameters: {
        pushParams: 'com.example.your_bundle_name' // 替换为你的包名
      }
    };
    context.startAbility(want).catch((err: BusinessError) => {
      console.error('Open settings failed:', err);
    });
  }
}

// 导出单例
export const permissionManager = PermissionManager.getInstance();


// -------------------------------------------------------------
// 2. 界面实战:相机权限申请
// -------------------------------------------------------------
@Entry
@Component
struct PermissionDemoPage {
  @State isCameraGranted: boolean = false;
  
  // 核心:在页面加载时检查状态
  async checkStatus() {
    this.isCameraGranted = await permissionManager.checkPermission('ohos.permission.CAMERA');
  }

  aboutToAppear(): void {
    this.checkStatus();
  }

  build() {
    Column() {
      // 顶部说明
      Text('权限管理实战')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 20 })

      // 状态展示
      Row() {
        Text('相机权限状态:')
          .fontSize(16)
        Text(this.isCameraGranted ? '已授权' : '未授权')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.isCameraGranted ? '#0A59F7' : '#FF4040')
      }
      .margin({ bottom: 40 })

      // 模拟相机功能区域
      if (this.isCameraGranted) {
        Column() {
          Stack() {
            Rect().width(200).height(200).fill('#333').radius(16)
            Image($r('app.media.app_icon')).width(50).height(50) // 模拟取景框
            Text('相机取景中...').fontColor(Color.White).margin({ top: 80 })
          }
        }
        .transition(TransitionEffect.OPACITY.animation({ duration: 300 }))
      } else {
        // 未授权时的占位图
        Column() {
          Text('权限被拒绝,无法使用相机')
            .fontColor('#999')
            .margin({ bottom: 20 })
          
          Button('申请相机权限')
            .onClick(async () => {
              // 获取上下文
              const context = getContext(this) as common.UIAbilityContext;
              
              // 发起申请
              const granted = await permissionManager.requestPermissions(context, ['ohos.permission.CAMERA']);
              
              if (granted) {
                this.isCameraGranted = true;
                promptAction.showToast({ message: '授权成功' });
              } else {
                // 如果被拒绝,弹出自定义引导
                promptAction.showDialog({
                  title: '权限申请',
                  message: '我们需要相机权限来提供拍照功能,请在设置中开启。',
                  buttons: [
                    { text: '取消', color: '#666666' },
                    { 
                      text: '去设置', 
                      color: '#0A59F7',
                      primary: true
                    }
                  ]
                }).then((resp) => {
                  if (resp.index === 1) {
                    // 跳转设置页
                    permissionManager.openSystemSettings(context);
                  }
                });
              }
            })
        }
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

五、 总结

鸿蒙的权限体系虽然严格,但逻辑非常清晰。从 module.json5 的静态声明,到运行时的动态申请,再到被拒绝后的二次引导,每一个环节都是为了保护用户的隐私安全。作为开发者,我们不应将此视为阻碍,而应将其视为建立用户信任的契机。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐