HarmonyOS应用权限管控全解析:从基础原理到企业级封装实战

105 阅读8分钟

✨家人们记得点个账号关注,会持续发布大前端领域技术文章💕 🍃

权限管控是鸿蒙应用开发的基础安全能力,用于保护用户隐私数据(如照片、通讯录)和系统资源(如相机、麦克风)。鸿蒙采用 “数据为中心” 的安全理念,通过分级授权、动态申请等机制,将隐私控制权交还给用户。本章将从权限基础、申请流程、封装实战、面试考点四个维度,带你彻底掌握鸿蒙权限管理。

一、概述

系统提供了一种允许应用访问系统资源(如:通讯录等)和系统能力(如:访问摄像头、麦克风等)的通用权限访问方式,来保护系统数据(包括用户个人数据)或功能,避免它们被不当或恶意使用。

应用权限保护的对象可以分为数据和功能:

  • 数据包括个人数据(如照片 优化了、通讯录、日历、位置等)、设备数据(如设备标识、相机、麦克风等)。
  • 功能包括设备功能(如访问摄像头/麦克风、打电话、联网等)、应用功能(如弹出悬浮窗、创建快捷方式等)。

编辑

二、授权级别

  • normal-都可以用
  • system_basic-需要签名(想表达后期应用上架了需要去申请权限 并且等待审核后才可以用)
  • system_core-三方无权

三、授权方式

根据授权方式的不同,权限类型可分为

system_grant(系统授权) 例如网络

如果在应用中申请了system_grant权限,那么系统会在用户安装应用时,自动把相应权限授予给应用。

user_grant(用户授权) 例如:麦克风和摄像头

该类型权限不仅需要在安装包中申请权限,还需要在应用动态运行时,通过发送弹窗的方式请求用户授权。在用户手动允许授权后,应用才会真正获取相应权限,从而成功访问操作目标对象。

四、申请应用权限

a. 在配置文件中声明权限

应用需要在module.json5配置文件的requestPermissions标签中声明权限。

{
"module": {
  "requestPermissions": [
      {
        "name" : "ohos.permission.xxxxxx", // 需要使用的权限名称
        // 申请权限的原因(用于录制音频)=> 必须去resources/base/element/string.json中声明(注意国际化)
        "reason": "$string:reason",  
        "usedScene": { // 权限使用的场景
          "abilities": [
            "EntryAbility" // 使用权限的UIAbility或者ExtensionAbility组件的名称
          ],
          "when":"inuse" // 调用时机:inuse-使用时, always-始终
        }
      },

b. 校验当前是否已经授权 checkAccessToken

在进行权限申请之前,需要先检查当前应用程序是否已经被授予权限。可以通过调用checkAccessToken()方法来校验当前是否已经授权。如果已经授权,则可以直接访问目标操作,否则需要进行下一步操作,即向用户申请授权。

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

const permissions: Array<Permissions> = ['ohos.permission.MICROPHONE'];

async function checkPermissionGrant(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;

  // 获取应用程序的accessTokenID
  let tokenId: number = 0;
  try {
    let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (error) {
    const err: BusinessError = error as BusinessError;
    console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
  }

  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (error) {
    const err: BusinessError = error as BusinessError;
    console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus;
}

async function checkPermissions(): Promise<void> {
  let grantStatus: abilityAccessCtrl.GrantStatus = await checkPermissionGrant(permissions[0]);

  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    // 已经授权,可以继续访问目标操作
  } else {
    // 申请麦克风权限
  }
}

c. 动态向用户申请授权 requestPermissionsFromUser

动态向用户申请权限是指在应用程序运行时向用户请求授权的过程。可以通过调用requestPermissionsFromUser()方法来实现。该方法接收一个权限列表参数,例如位置、日历、相机、麦克风等。用户可以选择授予权限或者拒绝授权。

可以在UIAbility的onWindowStageCreate()回调中调用requestPermissionsFromUser()方法来动态申请权限,也可以根据业务需要在UI中向用户申请授权。

应用在onWindowStageCreate()回调中申请授权时,需要等待异步接口loadContent()/setUIContent()执行结束后或在loadContent()/setUIContent()回调中调用requestPermissionsFromUser(),否则在Content加载完成前,requestPermissionsFromUser会调用失败。

应用在UIExtensionAbility申请授权时,需要在onWindowStageCreate函数执行结束后或在onWindowStageCreate函数回调中调用requestPermissionsFromUser(),否则在ability加载完成前,requestPermissionsFromUser会调用失败。

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

const permissions: Array<Permissions> = ['ohos.permission.MICROPHONE'];
// 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
  atManager.requestPermissionsFromUser(context, permissions).then((data) => {
    let grantStatus: Array<number> = data.authResults;
    let length: number = grantStatus.length;
    for (let i = 0; i < length; i++) {
      if (grantStatus[i] === 0) {
        // 用户授权,可以继续访问目标操作
      } else {
        // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
        return;
      }
    }
    // 授权成功
  }).catch((err: BusinessError) => {
    console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
  })
}

@Entry
@Component
struct Index {
  aboutToAppear() {
    // 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
    const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
    reqPermissionsFromUser(permissions, context);
  }

  build() {
    // ...
  }
}

d. 处理授权结果 requestPermissionOnSetting

如果用户授权,则可以继续访问目标操作。

如果用户拒绝授权,则需要提示用户必须授权才能访问当前页面的功能,并引导用户到系统应用“设置”中打开相应的权限。

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



// 三、 用户点击禁止之后 下次只能去应用授权页授权(手动打开权限)
function createPermissionsAgain(context:common.UIAbilityContext) {
  AlertDialog.show(
    {
      title: '温馨提示',
      message: '必须要授权才能使用,是否前往应用进行授权',
      autoCancel: false,
      alignment: DialogAlignment.Bottom,
      gridCount: 4,
      primaryButton: {
        value: '取消',
        action: () => {
          console.warn('qf 用户再次取消授权')
        }
      },
      secondaryButton: {
        value: '前往授权',
        action: () => {
          // https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/request-user-authorization-second-V5
          // 二次向用户申请授权
          // const context = getContext(this) as common.UIAbilityContext;
          context.startAbility({
            bundleName: 'com.huawei.hmos.settings', //设置应用bundleName
            abilityName: 'com.huawei.hmos.settings.MainAbility', //设置应用abilityName
            uri: "application_info_entry", //通知管理页面
            parameters: {
              pushParams: context.abilityInfo.bundleName
            }
          }).then((data) => {
            console.info('qf 前往授权页面成功', JSON.stringify(data))
          }).catch((error:Object) => {
            console.error('qf 前往授权页面失败', JSON.stringify(error))
          })
        }
      }
    }
  )
}


async function checkPermissionGrant(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;

  // 获取应用程序的accessTokenID
  let tokenId: number = 0;
  try {
    let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (error) {
    const err: BusinessError = error as BusinessError;
    console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
  }

  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (error) {
    const err: BusinessError = error as BusinessError;
    console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus;
}

async function checkPermissions(permission: Permissions): Promise<boolean> {
  // const grantStatus: boolean = await checkPermissionGrant('ohos.permission.MICROPHONE') === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;// 获取精确定位权限状态
  const grantStatus: boolean = await checkPermissionGrant(permission) === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;// 获取精确定位权限状态
  return grantStatus
}

// 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext
async function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): Promise<boolean> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
  return atManager.requestPermissionsFromUser(context, permissions).then((data) => {
    let grantStatus: Array<number> = data.authResults;
    let length: number = grantStatus.length;
    for (let i = 0; i < length; i++) {
      if (grantStatus[i] === 0) {
        // 用户授权,可以继续访问目标操作
        return true
      } else {
        createPermissionsAgain(context) // 打开设置页面,现在还未升级了  通过app内的弹出层提高用户体验度
        return false
        // // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
        // let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
        // // let context: Context = getContext(this) as common.UIAbilityContext;
        // // atManager.requestPermissionOnSetting(context, ['ohos.permission.APPROXIMATELY_LOCATION']).then((data: Array<abilityAccessCtrl.GrantStatus>) => {
        // return atManager.requestPermissionOnSetting(context, permissions).then((data: Array<abilityAccessCtrl.GrantStatus>) => {
        //   console.info('data:' + JSON.stringify(data));
        //   // return true
        //   return data[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
        // }).catch((err: BusinessError) => {
        //   console.error('data:' + JSON.stringify(err));
        //   return false
        // });
        // return false;
      }
    }
    return false
    // 授权成功
  }).catch((err: BusinessError) => {
    console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
    return false
  })
}

@Entry
@Component
struct Test {
  build() {
    Button('录音').onClick(async () => {
      // 方案1:直接调用录音的API   ❌   得看下权限有没有,如果没有去申请
      // 方案2:检查是否授权  有-直接用API,没有申请权限
      const state = await checkPermissions('ohos.permission.MICROPHONE')
      console.log('2 检查是否授权 ', state)
      if (!state) {
        // 3 动态获取权限
        const state2 = await reqPermissionsFromUser(['ohos.permission.MICROPHONE'], getContext(this) as common.UIAbilityContext);
        console.log('3 动态获取权限 ', state2)
        if (!state2) return
      }

      // 调用录音API实现各种功能
      console.log('调用录音API实现各种功能')
    })
  }
}

e. 封装权限管理

  • 封装
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';



class PermissionUtil {

  // 1 检查是否有权限  2个方法

  async checkPermissionGrant(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;

    // 获取应用程序的accessTokenID。
    let tokenId: number = 0;
    try {
      let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
      let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
      tokenId = appInfo.accessTokenId;
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to get bundle info for self. Code is ${err.code}, message is ${err.message}`);
    }

    // 校验应用是否被授予权限。
    try {
      grantStatus = await atManager.checkAccessToken(tokenId, permission);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`Failed to check access token. Code is ${err.code}, message is ${err.message}`);
    }

    return grantStatus;
  }

  async checkPermissions(permission: Permissions[], context: Context): Promise<boolean> {
    const bool: boolean = await this.checkPermissionGrant(permission[0]) === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;// 获取精确定位权限状态。
    // return bool  优化下
    if (!bool) return await this.reqPermissionsFromUser(permission, context)
    return true
  }


  // 2 动态申请权限
  async reqPermissionsFromUser(permissions: Array<Permissions>, context: Context): Promise<boolean> {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    return atManager.requestPermissionsFromUser(context, permissions).then(async (data) => {
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      for (let i = 0; i < length; i++) {
        if (grantStatus[i] === 0) {
          // 用户授权,可以继续访问目标操作
          // return true
        } else {
          // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
          // return false;
          return await this.onSetting(permissions, context);
        }
      }
      // 授权成功
      return true
    }).catch((err: BusinessError) => {
      return false
      console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`);
    })
  }

  // 3 再次申请权限 拉起一个窗口
  onSetting(permissions: Permissions[],  context:Context):Promise<boolean> {
    let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    // let context: Context = getContext(this) as common.UIAbilityContext;
    return atManager.requestPermissionOnSetting(context, permissions).then((data: Array<abilityAccessCtrl.GrantStatus>) => {
      console.info('data:' + JSON.stringify(data));
      return data[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
    }).catch((err: BusinessError) => {
      return false
      console.error('data:' + JSON.stringify(err));
    });
  }

}

export const permissionUtil = new PermissionUtil()
  • 使用
import { permissionUtil } from '../utils/PermissionUtil'

@Entry
@Component
struct Index {
  build() {
    Button('录音').onClick(async (event: ClickEvent) => {
       const state = await permissionUtil.checkPermissions(['ohos.permission.MICROPHONE'], getContext())
      if (!state) return
      console.log('录音API')
    })
  }
}

五、受限开放权限

编辑

a. 在配置文件中声明权限

module.json5

{
  "module": {
    "requestPermissions": [
    	....
    	继续追加

// 受限开放权限 ---------------------------------
      // 通讯录(权限组-读写)
      {
        "name": "ohos.permission.READ_CONTACTS",
        "reason": '$string:permission_reason_contact',
        "usedScene": {}
      },
      {
        "name": "ohos.permission.WRITE_CONTACTS",
        "reason": '$string:permission_reason_contact',
        "usedScene": {}
      },
      // 图片或视频文件(权限组-读写 仅应用需要克隆、备份或同步图片/视频类文件可申请) 其他场景用使用Picker选择媒体库资源。
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:permission_reason_imageVideo",
        "usedScene": {}
      },
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:permission_reason_imageVideo",
        "usedScene": {}
      }
      // 受限开放权限 ---------------------------------

b. 自动签名

配置好声明权限,直接模拟器运行会报错,因为这些权限需要签名(目的就是告诉开发者这些权限不是说直接就可以用,上架的时候需要审批通过才可以用)

developer.huawei.com/consumer/cn…

配置完在运行就没事了,

然后就可以使用测试

六、附言

APL(Ability Privilege Level)应用的权限申请优先级的定义,不同等级的应用能够申请的权限等级不同。
ACL(Access Control List)访问控制列表   (也就是受限开放权限里面的权限,老手册会有标记现在新的手册没有了)
AGC(AppGallery Connect) 应用市场
​
权限级别:normal-都可以用、system_basic-需要签名、system_core-三方无权 (APL表达的思想)
授权方式:
- system_grant系统授权-用户安装应用时自动授权后续无感:internet网络/蓝牙配置/应用基本信息/.../身份认证/Wi-Fi信息/陀螺仪传感器信息等等
- user_grant用户授权-弹框:相机/位置信息/媒体文件/麦克风/日历/用户运动状态/设备数据交换/用户健康等等

应用在访问数据或者执行操作时,需要评估该行为是否需要应用具备相关的权限。如果确认需要目标权限,则需要在应用安装包中申请目标权限。每一个权限的权限等级、授权方式不同,申请权限的方式也不同,开发者在申请权限前,需要先根据以下流程判断应用能否申请目标权限。

编辑

上图的数字标注,请参考以下说明:

  • 标注1:应用APL等级与权限等级的匹配关系请参考APL等级说明

  • 标注2:权限的授权方式分为user_grant(用户授权)和system_grant(系统授权),具体请参考授权方式说明

    • 如果目标权限是system_grant类型,开发者在进行权限申请后,系统会在安装应用时自动为其进行权限预授予,开发者不需要做其他操作即可使用权限。

    • 在应用需要获取user_grant权限时,请完成以下步骤:

      • 在配置文件中,声明应用需要请求的权限。
      • 将应用中需要申请权限的目标对象与对应目标权限进行关联,让用户明确地知道,哪些操作需要用户向应用授予指定的权限。
      • 运行应用时,在用户触发访问操作目标对象时应该调用接口,精准触发动态授权弹框。该接口的内部会检查当前用户是否已经授权应用所需的权限,如果当前用户尚未授予应用所需的权限,该接口会拉起动态授权弹框,向用户请求授权。
      • 检查用户的授权结果,确认用户已授权才可以进行下一步操作。
  • 标注3:应用可以通过ACL(访问控制列表)方式申请高级别的权限,具体请参考申请使用受限权限

七、企业级面试题

 鸿蒙授权方式、授权级别有哪些

 申请权限步骤

a 在配置文件中声明权限

b 校验当前是否已经授权 checkAccessToken

c 动态向用户申请授权 requestPermissionsFromUser

d 处理授权结果 requestPermissionOnSetting 如果用户授权,则可以继续访问目标操作。 如果用户拒绝授权,则需要提示用户必须授权才能访问当前页面的功能,并引导用户到系统应用“设置”中打开相应的权限。 在common架构base模块下封装授权文件

developer.huawei.com/consumer/cn…

使用ACL/受限制开放权限如何申请

developer.huawei.com/consumer/cn…

​编辑​

八、鸿蒙安全 - 全新的架构与机制,将隐私控制权交还给用户

首先,鸿蒙 NEXT 全面梳理了所有系统授权,取消了 9 类不合理权限,有效避免了潜在的隐私泄露风险。这些权限包括读取已安装应用列表、访问短信、访问存储文件、创建桌面快捷方式、获取设备信息、显示悬浮窗、访问通话记录、应用内安装应用以及拨打电话和管理通话

img​编辑​

其次,鸿蒙 NEXT 改变了传统的权限控制逻辑,从“管权限”转变为“管数据” ,从而为用户提供更高级别的隐私保护。例如,在使用支付宝或小红书等第三方应用进行拍照或扫码时,传统系统需要调用摄像头权限完成此次操作,拍摄或扫码时,应用可以看到整个过程中摄像头实时扫过的画面。而在鸿蒙 NEXT 上,当用户在第三方应用中点击拍摄时,系统提示变为“您的摄像头正在取景,但应用仅可访问拍摄完成的图片”。

鸿蒙 NEXT 这一创新的安全访问机制,涵盖了位置、文件、图库、相机、联系人、剪贴板和音频 7 个方面,这些高频使用场景,也是用户隐私风险泄露的高发区。通过精准性、单次性的权限授予方式,确保了最小化的数据访问,大大降低了用户隐私泄露的风险。可以说是把隐私控制权完全交还给用户,让用户能够真正掌控。

img​编辑​

举个简单的例子,假设用户的隐私数据是家中的珍贵物品,而“权限”则如同一把把房间钥匙。过去,当用户点击“允许”授权时,实际上是交出了钥匙,应用便可以自由进入该房间并取用其中的物品。然而,在 HarmonyOS NEXT 上,情况发生了变化。应用只能收到用户授权的具体物品,房间里的其他东西,都与之隔绝。例如修改头像选择照片

​编辑​

鸿蒙开发者班级

✨家人们点个juejin账号关注,会持续发布大前端领域技术文章💕 🍃

✨家人们点个juejin账号关注,会持续发布大前端领域技术文章💕 🍃

✨家人们点个juejin账号关注,会持续发布大前端领域技术文章💕 🍃

    ^_^ 点关注、不迷路、主播带你学技术 (๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤