📖 鸿蒙NEXT开发实战系列 | 第34篇 | 进阶篇 🎯 适合人群:有鸿蒙基础的开发者 ⏰ 阅读时间:约12分钟 | 💻 开发环境:DevEco Studio 5.0+


系列导航系列简介 | 上一篇:状态管理进阶 | 下一篇:数据持久化方案


📑 目录


一、为什么需要权限管理

在移动应用开发中,权限管理是保障用户隐私和系统安全的核心机制。鸿蒙系统(HarmonyOS NEXT)采用严格的权限管理体系:

权限管理的重要性

维度

说明

用户隐私保护

防止应用过度获取用户敏感信息

系统安全保障

限制应用对系统资源的访问

应用上架审核

权限申请不合规会导致审核驳回

用户体验

合理的权限申请提升用户信任度

常见需要权限的功能

  • 访问相机、麦克风

  • 读取/写入存储

  • 获取位置信息

  • 访问通讯录、通话记录

  • 蓝牙、NFC连接


二、鸿蒙权限类型分类

鸿蒙系统将权限分为三大类,不同类型权限的申请方式不同:

2.1 普通权限(Normal Permissions)

普通权限不涉及用户敏感数据,系统自动授予,无需用户手动确认。

特征:
- 风险等级低
- 系统自动授予
- 无需运行时申请

常见普通权限

权限名称

说明

ohos.permission.INTERNET

访问网络

ohos.permission.GET_NETWORK_INFO

获取网络状态

ohos.permission.GET_WIFI_INFO

获取WiFi信息

ohos.permission.USE_BLUETOOTH

使用蓝牙

2.2 系统权限(System Basic Permissions)

系统权限需要应用在应用市场审核通过后才能获取。

特征:
- 需要应用市场审核
- 涉及系统级功能
- 申请门槛较高

2.3 敏感权限(User Grants Permissions)

敏感权限涉及用户隐私数据,必须在运行时向用户申请并获得明确授权。

特征:
- 涉及用户隐私
- 必须运行时申请
- 需要用户明确同意
- 用户可随时撤销

常见敏感权限

权限名称

说明

权限组

ohos.permission.CAMERA

相机

相机

ohos.permission.READ_MEDIA

读取媒体文件

存储

ohos.permission.WRITE_MEDIA

写入媒体文件

存储

ohos.permission.APPROXIMATELY_LOCATION

大致位置

位置

ohos.permission.LOCATION

精确位置

位置

ohos.permission.MICROPHONE

麦克风

麦克风

ohos.permission.READ_CONTACTS

读取通讯录

通讯录

2.4 权限配置方式

module.json5 中声明所需权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:location_permission_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

配置说明

  • name:权限名称

  • reason:申请权限的原因(支持多语言)

  • usedScene.when:权限使用时机(always 始终使用,inuse 仅使用时)


三、权限申请完整流程

3.1 运行时权限申请流程图

┌─────────────────────────────────────────────────────────────┐
│                       权限申请流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐  │
│  │ 检查权限状态  │───→│ 已授权?     │───→│   执行功能    │  │
│  └──────────────┘    └──────┬───────┘    └──────────────┘  │
│                             │                               │
│                             │ 未授权                        │
│                             ▼                               │
│                     ┌──────────────┐                        │
│                     │ 申请权限     │                        │
│                     └──────┬───────┘                        │
│                             │                               │
│                             ▼                               │
│                     ┌──────────────┐                        │
│                     │ 用户授权?   │                        │
│                     └──────┬───────┘                        │
│                             │                               │
│              ┌──────────────┼──────────────┐                │
│              ▼              ▼              ▼                │
│        ┌──────────┐  ┌──────────┐  ┌──────────┐           │
│        │  允许    │  │  拒绝    │  │ 永久拒绝  │           │
│        └────┬─────┘  └────┬─────┘  └────┬─────┘           │
│             │             │             │                  │
│             ▼             ▼             ▼                  │
│        执行功能      提示用户     引导至设置页              │
│                      并记录                                │
└─────────────────────────────────────────────────────────────┘

3.2 基础权限申请代码

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

// 权限申请核心方法
async function requestPermissions(context: common.UIAbilityContext, permissions: string[]): Promise<boolean> {
  try {
    // 1. 创建权限管理器
    const atManager = abilityAccessCtrl.createAtManager();
    
    // 2. 申请权限
    const results = await atManager.requestPermissionsFromUser(context, permissions);
    
    // 3. 检查结果
    const allGranted = results.authResults.every(result => 
      result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    );
    
    if (allGranted) {
      console.info('所有权限已授予');
      return true;
    } else {
      // 4. 处理被拒绝的权限
      const deniedPermissions = permissions.filter((_, index) => 
        results.authResults[index] !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
      );
      console.warn('以下权限被拒绝:', deniedPermissions);
      return false;
    }
  } catch (err) {
    console.error(`权限申请失败: ${err}`);
    return false;
  }
}

3.3 在页面中使用

import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct PermissionDemo {
  @State cameraGranted: boolean = false;
  private context = getContext(this) as common.UIAbilityContext;

  // 申请相机权限
  async requestCameraPermission() {
    const permission: string[] = ['ohos.permission.CAMERA'];
    const granted = await requestPermissions(this.context, permission);
    
    if (granted) {
      this.cameraGranted = true;
      promptAction.showToast({ message: '相机权限已授予' });
    } else {
      promptAction.showToast({ message: '需要相机权限才能拍照' });
    }
  }

  build() {
    Column() {
      Button('申请相机权限')
        .onClick(() => this.requestCameraPermission())
        .margin({ bottom: 10 })

      if (this.cameraGranted) {
        Text('✅ 相机权限已授权')
          .fontColor('#00CC66')
      }
    }
    .padding(20)
  }
}

四、权限组管理机制

4.1 什么是权限组

权限组是将功能相关的权限归为一组,当用户授予组内任一权限时,组内其他权限也会被自动授予。

鸿蒙主要权限组

权限组

包含权限

说明

位置

ohos.permission.APPROXIMATELY_LOCATION, ohos.permission.LOCATION

位置相关

存储

ohos.permission.READ_MEDIA, ohos.permission.WRITE_MEDIA

存储访问

相机

ohos.permission.CAMERA

相机访问

麦克风

ohos.permission.MICROPHONE

麦克风访问

通讯录

ohos.permission.READ_CONTACTS, ohos.permission.WRITE_CONTACTS

通讯录访问

4.2 权限组申请最佳实践

// 权限组定义
const PERMISSION_GROUPS = {
  LOCATION: [
    'ohos.permission.APPROXIMATELY_LOCATION',
    'ohos.permission.LOCATION'
  ],
  STORAGE: [
    'ohos.permission.READ_MEDIA',
    'ohos.permission.WRITE_MEDIA'
  ],
  CAMERA: [
    'ohos.permission.CAMERA'
  ],
  MICROPHONE: [
    'ohos.permission.MICROPHONE'
  ]
};

// 申请权限组
async function requestPermissionGroup(
  context: common.UIAbilityContext, 
  groupKey: keyof typeof PERMISSION_GROUPS
): Promise<boolean> {
  const permissions = PERMISSION_GROUPS[groupKey];
  return await requestPermissions(context, permissions);
}

4.3 运行时权限检查

在申请权限前,先检查权限是否已授予:

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

// 检查权限是否已授权
async function checkPermission(context: common.UIAbilityContext, permission: string): Promise<boolean> {
  try {
    const atManager = abilityAccessCtrl.createAtManager();
    const result = await atManager.checkAccessToken(context.tokenID, permission);
    return result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
  } catch (err) {
    console.error(`检查权限失败: ${err}`);
    return false;
  }
}

// 使用示例
async function checkCameraPermission(context: common.UIAbilityContext): Promise<boolean> {
  return await checkPermission(context, 'ohos.permission.CAMERA');
}

五、自定义权限弹窗

5.1 为什么需要自定义弹窗

系统默认的权限弹窗样式固定,自定义弹窗可以:

  • 提供更详细的权限用途说明

  • 提升用户体验和信任度

  • 减少用户拒绝率

5.2 自定义权限说明弹窗

import { promptAction } from '@kit.ArkUI';

interface PermissionDialogOptions {
  title: string;
  message: string;
  confirmText?: string;
  cancelText?: string;
  permissionName: string;
}

// 自定义权限说明弹窗
function showPermissionDialog(options: PermissionDialogOptions): Promise<boolean> {
  return new Promise((resolve) => {
    promptAction.showDialog({
      title: options.title,
      message: options.message,
      buttons: [
        {
          text: options.cancelText || '暂不授权',
          color: '#999999'
        },
        {
          text: options.confirmText || '去授权',
          color: '#007AFF'
        }
      ]
    }).then((result) => {
      resolve(result.index === 1);
    });
  });
}

5.3 权限申请前弹窗示例

import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct PermissionWithDialog {
  private context = getContext(this) as common.UIAbilityContext;

  // 带说明弹窗的权限申请
  async requestCameraWithDialog() {
    // 1. 显示权限说明弹窗
    const userConfirmed = await showPermissionDialog({
      title: '需要相机权限',
      message: '为了方便您拍摄证件照、扫描二维码等功能,我们需要获取您的相机权限。',
      confirmText: '同意并继续',
      cancelText: '暂不使用'
    });

    if (!userConfirmed) {
      promptAction.showToast({ message: '您已取消相机权限申请' });
      return;
    }

    // 2. 用户同意后申请权限
    const granted = await requestPermissions(this.context, ['ohos.permission.CAMERA']);
    
    if (granted) {
      promptAction.showToast({ message: '相机权限已授予' });
    }
  }

  build() {
    Column() {
      Button('申请相机权限(带说明)')
        .onClick(() => this.requestCameraWithDialog())
    }
    .padding(20)
  }
}

六、权限工具类封装

6.1 完整的权限管理工具类

// PermissionManager.ts
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';

/**
 * 权限管理工具类
 * 提供权限申请、检查、弹窗等功能
 */
export class PermissionManager {
  private atManager: abilityAccessCtrl.AtManager;
  private context: common.UIAbilityContext;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
    this.atManager = abilityAccessCtrl.createAtManager();
  }

  /**
   * 检查权限是否已授权
   * @param permission 权限名称
   * @returns 是否已授权
   */
  async checkPermission(permission: string): Promise<boolean> {
    try {
      const result = await this.atManager.checkAccessToken(this.context.tokenID, permission);
      return result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
    } catch (err) {
      console.error(`检查权限失败: ${err}`);
      return false;
    }
  }

  /**
   * 检查多个权限是否已全部授权
   * @param permissions 权限列表
   * @returns 是否全部授权
   */
  async checkPermissions(permissions: string[]): Promise<boolean> {
    const results = await Promise.all(
      permissions.map(permission => this.checkPermission(permission))
    );
    return results.every(granted => granted);
  }

  /**
   * 申请单个权限
   * @param permission 权限名称
   * @param reason 申请原因(可选,用于自定义弹窗)
   * @returns 是否授权成功
   */
  async requestPermission(permission: string, reason?: string): Promise<boolean> {
    return await this.requestPermissions([permission], reason);
  }

  /**
   * 申请多个权限
   * @param permissions 权限列表
   * @param reason 申请原因(可选,用于自定义弹窗)
   * @returns 是否全部授权成功
   */
  async requestPermissions(permissions: string[], reason?: string): Promise<boolean> {
    try {
      // 1. 检查是否已授权
      const alreadyGranted = await this.checkPermissions(permissions);
      if (alreadyGranted) {
        return true;
      }

      // 2. 如果有自定义原因,显示说明弹窗
      if (reason) {
        const userConfirmed = await this.showPermissionDialog(reason);
        if (!userConfirmed) {
          return false;
        }
      }

      // 3. 申请权限
      const results = await this.atManager.requestPermissionsFromUser(this.context, permissions);
      
      // 4. 检查结果
      const allGranted = results.authResults.every(result => 
        result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
      );

      return allGranted;
    } catch (err) {
      console.error(`申请权限失败: ${err}`);
      return false;
    }
  }

  /**
   * 显示权限说明弹窗
   * @param message 说明信息
   * @returns 用户是否同意
   */
  private async showPermissionDialog(message: string): Promise<boolean> {
    return new Promise((resolve) => {
      promptAction.showDialog({
        title: '权限申请',
        message: message,
        buttons: [
          {
            text: '拒绝',
            color: '#999999'
          },
          {
            text: '同意',
            color: '#007AFF'
          }
        ]
      }).then((result) => {
        resolve(result.index === 1);
      });
    });
  }

  /**
   * 显示权限被拒绝提示
   * @param permissionName 权限名称(中文描述)
   */
  showPermissionDeniedToast(permissionName: string): void {
    promptAction.showToast({ 
      message: `${permissionName}权限被拒绝,功能无法使用` 
    });
  }
}

6.2 权限常量定义

// PermissionConstants.ts
export const Permissions = {
  // 相机
  CAMERA: 'ohos.permission.CAMERA',
  
  // 位置
  APPROXIMATELY_LOCATION: 'ohos.permission.APPROXIMATELY_LOCATION',
  LOCATION: 'ohos.permission.LOCATION',
  
  // 存储
  READ_MEDIA: 'ohos.permission.READ_MEDIA',
  WRITE_MEDIA: 'ohos.permission.WRITE_MEDIA',
  
  // 麦克风
  MICROPHONE: 'ohos.permission.MICROPHONE',
  
  // 通讯录
  READ_CONTACTS: 'ohos.permission.READ_CONTACTS',
  WRITE_CONTACTS: 'ohos.permission.WRITE_CONTACTS',
  
  // 蓝牙
  ACCESS_BLUETOOTH: 'ohos.permission.ACCESS_BLUETOOTH',
  
  // 通知
  ENABLE_NOTIFICATION: 'ohos.permission.ENABLE_NOTIFICATION'
} as const;

// 权限组定义
export const PermissionGroups = {
  LOCATION: [Permissions.APPROXIMATELY_LOCATION, Permissions.LOCATION],
  STORAGE: [Permissions.READ_MEDIA, Permissions.WRITE_MEDIA],
  CONTACTS: [Permissions.READ_CONTACTS, Permissions.WRITE_CONTACTS],
  CAMERA: [Permissions.CAMERA],
  MICROPHONE: [Permissions.MICROPHONE]
} as const;

6.3 使用示例

import { common } from '@kit.AbilityKit';
import { PermissionManager, Permissions, PermissionGroups } from './PermissionManager';

@Entry
@Component
struct PermissionUsageDemo {
  private permissionManager: PermissionManager;
  @State locationGranted: boolean = false;

  constructor() {
    super();
    this.permissionManager = new PermissionManager(getContext(this) as common.UIAbilityContext);
  }

  // 申请相机权限
  async handleCamera() {
    const granted = await this.permissionManager.requestPermission(
      Permissions.CAMERA,
      '为了拍摄照片,我们需要使用您的相机权限'
    );
    
    if (granted) {
      // 执行相机相关功能
      console.info('相机权限已授权');
    } else {
      this.permissionManager.showPermissionDeniedToast('相机');
    }
  }

  // 申请位置权限组
  async handleLocation() {
    const granted = await this.permissionManager.requestPermissions(
      PermissionGroups.LOCATION,
      '为了提供导航服务,我们需要获取您的位置信息'
    );
    
    this.locationGranted = granted;
  }

  // 检查权限状态
  async checkCameraStatus() {
    const granted = await this.permissionManager.checkPermission(Permissions.CAMERA);
    console.info(`相机权限状态: ${granted ? '已授权' : '未授权'}`);
  }

  build() {
    Column({ space: 15 }) {
      Button('申请相机权限')
        .onClick(() => this.handleCamera())
        .width('100%')

      Button('申请位置权限')
        .onClick(() => this.handleLocation())
        .width('100%')

      Button('检查相机权限状态')
        .onClick(() => this.checkCameraStatus())
        .width('100%')

      if (this.locationGranted) {
        Text('位置权限已授权')
          .fontColor('#00CC66')
      }
    }
    .padding(20)
  }
}

七、常见问题与解决方案

7.1 权限申请被永久拒绝

问题:用户多次拒绝后,系统不再显示权限弹窗。

解决方案

import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { router } from '@kit.ArkUI';

async function requestPermissionWithFallback(
  context: common.UIAbilityContext, 
  permission: string
): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  
  try {
    const result = await atManager.requestPermissionsFromUser(context, [permission]);
    
    if (result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      return true;
    }
    
    // 检查是否需要引导用户去设置页
    // 注意:当前API可能需要使用其他方式判断
    showGoToSettingsDialog();
    return false;
  } catch (err) {
    console.error(`申请权限失败: ${err}`);
    return false;
  }
}

function showGoToSettingsDialog() {
  promptAction.showDialog({
    title: '权限申请',
    message: '该权限被拒绝,请前往设置页面手动开启',
    buttons: [
      { text: '取消', color: '#999999' },
      { text: '去设置', color: '#007AFF' }
    ]
  }).then((result) => {
    if (result.index === 1) {
      // 跳转到应用设置页
      // 实际实现需要根据系统版本调整
    }
  });
}

7.2 权限申请时机选择

最佳实践

场景

建议

应用启动时

仅申请核心必需权限

功能触发时

在用户点击相关功能时申请

首次使用

显示功能说明后再申请

7.3 权限状态监听

// 注意:当前HarmonyOS NEXT可能不支持直接监听权限变化
// 建议在关键操作前检查权限状态
async function ensurePermissionBeforeAction(
  permissionManager: PermissionManager,
  permission: string,
  action: () => Promise<void>
): Promise<void> {
  const granted = await permissionManager.checkPermission(permission);
  
  if (granted) {
    await action();
  } else {
    const newGranted = await permissionManager.requestPermission(permission);
    if (newGranted) {
      await action();
    }
  }
}

八、总结与最佳实践

8.1 权限管理最佳实践

  1. 最小权限原则:只申请必要的权限

  2. 明确说明用途:在申请前向用户解释为什么需要该权限

  3. 优雅处理拒绝:权限被拒绝时提供替代方案

  4. 及时释放权限:不再需要时释放权限(如关闭后台定位)

  5. 定期检查权限:在关键功能执行前检查权限状态

8.2 代码组织建议

src/
├── common/
│   └── permission/
│       ├── PermissionManager.ts    // 权限管理类
│       └── PermissionConstants.ts  // 权限常量
├── utils/
│   └── permissionHelper.ts         // 辅助函数
└── pages/
    └── SettingsPage.tsx            // 权限设置页面

8.3 审核注意事项

  • 权限申请原因必须真实、准确

  • 不能在用户未触发功能时强制申请权限

  • 权限拒绝后应有降级方案

  • 敏感权限(位置、相机等)需要特别说明用途


📚 系列文章推荐


🏷️ 标签

鸿蒙权限 权限管理 HarmonyOS 安全开发 上架审核 运行时权限 权限组 隐私保护


💡 下期预告:《鸿蒙NEXT数据持久化方案全解析》- 涵盖首选项、关系型数据库、分布式数据管理等内容。


版权声明:本文为原创技术文章,转载请注明出处。

反馈交流:如有问题或建议,欢迎在评论区留言交流。

Logo

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

更多推荐