踩坑记录23:权限申请流程与用户拒绝处理

阅读时长:12分钟 | 难度等级:高级 | 适用版本:HarmonyOS NEXT (API 12+)
关键词:权限申请、动态授权、用户拒绝、引导设置
声明:本文基于真实项目开发经历编写,所有代码片段均来自实际踩坑场景。

欢迎加入开源鸿蒙PC社区https://harmonypc.csdn.net/
项目 Git 仓库https://atomgit.com/Dgr111-space/HarmonyOS


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

📖 前言导读

当你的 HarmonyOS 项目需要踩坑记录23:权限申请流程与用户拒绝处理时,本文提供的一套完整方案可以帮你少走弯路。所有代码均来自生产环境验证,涵盖正常流程和异常边界情况的处理。

踩坑记录23:权限申请流程与用户拒绝处理

严重程度:⭐⭐⭐⭐ | 发生频率:高
涉及模块:@ohos.abilityAccessCtrl、权限声明、用户体验

一、问题现象

  1. 应用直接崩溃——缺少权限却使用了对应 API
  2. 权限弹窗弹出时机不对,用户感到突兀
  3. 用户拒绝权限后应用功能完全不可用

二、权限体系的层次

HarmonyOS 权限模型

系统权限

normal 级别

system_core 级别

sensitive 级别

安装时授权
无需弹窗

动态申请 + 弹窗

动态申请 + 弹窗
用户可能永久拒绝

级别 授权方式 示例 用户感知
normal 安装时自动授予 网络访问、网络信息
system_core 动态申请 蓝牙、定位(粗略) 弹窗一次
sensitive 动态申请 精确定位、相机、麦克风 弹窗 + 可永久拒绝

三、完整的权限申请流程

步骤 1:module.json5 声明

// entry/src/main/module.json5
{
  module: {
    requestPermissions: [
      {
        name: 'ohos.permission.INTERNET',           // normal 级别
        reason: '$string:permission_internet_reason',
        usedScene: {
          abilities: ['EntryAbility'],
          when: 'always'
        }
      },
      {
        name: 'ohos.permission.CAMERA',              // sensitive 级别
        reason: '$string:permission_camera_reason',
        usedScene: {
          abilities: ['EntryAbility'],
          when: 'inuse'
        }
      },
      {
        name: 'ohos.permission.LOCATION',            // sensitive 级别
        reason: '$string:permission_location_reason',
        usedScene: {
          abilities: ['EntryAbility'],
          when: 'inuse'
        }
      }
    ]
  }
}

步骤 2:字符串资源

// resources/base/element/string.json
{
  "string": [
    {
      "name": "permission_internet_reason",
      "value": "用于加载数据内容和同步信息"
    },
    {
      "name": "permission_camera_reason",
      "value": "用于扫描二维码和拍摄照片"
    },
    {
      "name": "permission_location_reason",
      "value": "用于获取附近的位置信息"
    }
  ]
}

步骤 3:运行时动态申请

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

export enum PermissionResult {
  GRANTED = 'GRANTED',
  DENIED = 'DENIED',
  PERMANENTLY_DENIED = 'PERMANENTLY_DENIED',
  NOT_DETERMINED = 'NOT_DETERMINED'
}

export class PermissionManager {

  /**
   * 检查单个权限状态
   */
  static async checkPermission(permission: string): Promise<PermissionResult> {
    try {
      const atManager = abilityAccessCtrl.createAtManager()
      const context = getContext() as common.UIAbilityContext
      
      const grantStatus = await atManager.checkAccessToken(
        context.applicationInfo.accessTokenId, 
        permission
      )
      
      switch (grantStatus) {
        case abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED:
          return PermissionResult.GRANTED
        default:
          return PermissionResult.DENIED
      }
    } catch (e) {
      console.error('[PermissionManager] check failed:', e)
      return PermissionResult.NOT_DETERMINED
    }
  }

  /**
   * 申请单个权限
   */
  static async requestPermission(
    permission: string
  ): Promise<PermissionResult> {
    // 先检查当前状态
    const current = await this.checkPermission(permission)
    if (current === PermissionResult.GRANTED) {
      return PermissionResult.GRANTED
    }
    
    try {
      const atManager = abilityAccessCtrl.createAtManager()
      
      const result = await atManager.requestPermissionsFromUser(
        getContext() as common.UIAbilityContext,
        [permission]
      )
      
      const authResults = result.authResults
      if (authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
        return PermissionResult.GRANTED
      } else {
        // 检查是否为永久拒绝(用户勾选了"不再询问")
        return PermissionResult.PERMANENTLY_DENIED
      }
    } catch (e) {
      console.error('[PermissionManager] request failed:', e)
      return PermissionResult.DENIED
    }
  }

  /**
   * 申请多个权限(按顺序)
   */
  static async requestPermissions(
    permissions: string[]
  ): Promise<Map<string, PermissionResult>> {
    const results = new Map<string, PermissionResult>()
    
    for (const perm of permissions) {
      results.set(perm, await this.requestPermission(perm))
    }
    
    return results
  }

  /**
   * 引导用户去设置页开启权限
   */
  static openAppSettings() {
    const context = getContext() as common.UIAbilityContext
    context.startAbility({
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri: 'application_info_entry',
      parameters: {
        // 打开当前应用的设置页面
      }
    }).catch((err: BusinessError) => {
      console.error('[PermissionManager] open settings failed:', err)
    })
  }
}

步骤 4:在 UI 中的集成

@Component
struct CameraFeature {
  @State cameraGranted: PermissionResult = PermissionResult.NOT_DETERMINED
  @State showRationale: boolean = false

  async aboutToAppear() {
    // 进入页面时检查权限
    this.cameraGranted = await PermissionManager.checkPermission(
      'ohos.permission.CAMERA'
    )
  }

  async requestCameraPermission() {
    this.cameraGranted = await PermissionManager.requestPermission(
      'ohos.permission.CAMERA'
    )

    if (this.cameraGranted === PermissionResult.GRANTED) {
      // 权限已授予,开始拍照
      this.openCamera()
    } else if (this.cameraGranted === PermissionResult.PERMANENTLY_DENIED) {
      // 用户之前选择了"永久拒绝"
      this.showRationale = true
    }
    // 如果只是暂时拒绝,用户可以再次尝试
  }

  openCamera() {
    // 执行相机相关逻辑
    console.log('Camera opened!')
  }

  goToSettings() {
    PermissionManager.openAppSettings()
    this.showRationale = false
  }

  build() {
    Column() {
      if (this.cameraGranted === PermissionResult.GRANTED) {
        // 已有权限,显示功能 UI
        this.CameraPreview()
      } else {
        // 未授权,显示引导 UI
        Column({ space: 20 }) {
          Text('\U0001F4F8').fontSize(48)
          
          Text('需要相机权限')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            
          if (this.showRationale) {
            // 永久拒绝时的特殊提示
            Text('您已禁止了相机权限。如需使用此功能,请在设置中手动开启。')
              .fontSize(14)
              .fontColor('#909399')
              .textAlign(TextAlign.Center)
              
            HButton({
              btnText: '去设置',
              btnType: 'primary',
              onButtonClick: () => this.goToSettings()
            })
          } else {
            Text('本功能需要使用您的相机来扫描和拍照')
              .fontSize(14)
              .fontColor('#606266')
              .textAlign(TextAlign.Center)
            
            HButton({
              btnText: '授权相机',
              btnType: 'primary',
              onButtonClick: () => this.requestCameraPermission()
            })
          }
        }
        .width('100%')
        .margin({ top: 80 })
      }
    }
    .width('100%').height('100%')
  }

  @Builder CameraPreview() {
    // 相机预览组件...
    Text('相机预览区域').fontSize(14)
  }
}

四、用户体验原则

需要权限?

是否核心功能?

首次进入即申请

用到时再申请

用户同意?

✅ 正常使用

是暂时拒绝?

稍后可再次申请

永久拒绝

引导去设置页

提供降级方案

不强制退出
保持其他功能可用

原则 做法
按需申请 不要一启动就索要所有权限
说明原因 清楚告知为什么需要这个权限
尊重拒绝 拒绝后提供降级体验,不要反复弹窗
记住选择 不要每次都问同一个权限
引导路径 永久拒绝时提供清晰的设置入口

参考资源与延伸阅读

官方文档

> 系列导航:本文是「HarmonyOS 开发踩坑记录」系列的第 23 篇。该系列共 30 篇,涵盖 ArkTS 语法、组件开发、状态管理、网络请求、数据库、多端适配等全方位实战经验。

工具与资源### 工具与资源


👇 如果这篇对你有帮助,欢迎点赞、收藏、评论!

你的支持是我持续输出高质量技术内容的动力 💪

Logo

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

更多推荐