HarmonyOS 6.0 权限管理与动态授权 — 应用审核被拒的血泪教训
对应Demo: PermissionDemo | 难度: 高级 | 关键词: abilityAccessCtrl, requestPermissionsFromUser, checkAccessToken, Permissions
我第一个提交华为应用市场的HarmonyOS应用,审核直接被拒了。拒审理由写了一堆,核心就一条——“应用在使用相机功能时未动态申请权限”。我当时心想,我module.json5里不是声明了ohos.permission.CAMERA权限吗?怎么还说没申请?后来才知道,HarmonyOS的权限分两种——normal类型的安装时自动授予,但user_grant类型的必须运行时动态申请,弹窗让用户确认。我光在json5里声明了,代码里压根没调用requestPermissionsFromUser,难怪被拒。
更搞笑的是,我后来加上动态申请的代码,又因为权限弹窗的reason字符串写了中文原文而不是$string:资源引用,再次被拒。审核标准那个严啊,你写"需要使用相机"不够具体,要写"需要使用相机拍照以完成头像设置"才行;你写中文原文不行,必须用$string:资源引用。我的应用前前后后提交了三次才过审,每次被拒的理由都不一样,但都是权限相关的问题。
还有个隐蔽的坑——我一开始的代码是先checkAccessToken检查权限状态,发现未授权后再requestPermissionsFromUser申请。看起来逻辑没问题对吧?但如果用户之前选了"拒绝且不再询问",requestPermissionsFromUser就不会再弹窗了,直接返回拒绝。我的代码以为会弹窗,一直等着用户操作,结果等了个寂寞,卡在那了。
我后来仔细回想了一下,整个权限功能的开发大概花了我三天——第一天被拒审然后排查原因,第二天加上动态申请代码又因为reason格式被拒,第三天把所有细节搞对终于过审。三天时间就为了搞个权限申请,这在Android上半天就能搞定的事。但HarmonyOS的审核确实更严格,每个细节都要对,一个不对就打回。特别是reason的写法,我见过有人写了五六个版本才通过,审核人员觉得reason跟功能不匹配就给你打回来。
还有个很多人不知道的事情——权限申请的时机也很重要。你一进入应用就弹一堆权限申请弹窗,审核可能不通过,因为用户体验差。审核人员希望看到的是"按需申请"——用户点击拍照按钮时才申请相机权限,点击发送语音时才申请麦克风权限。这种交互设计更合理,也是HarmonyOS推荐的做法。
今天这篇,我把权限相关的坑全部写出来,帮你们一次过审,少走弯路。

两种权限类型:normal vs user_grant
这是最基础的概念,搞不清这个后面全白搭。我第一天做权限功能的时候,根本不知道权限还分两种,以为声明了就自动拥有。结果相机死活打不开,日志里全是权限错误。
normal权限
安装时自动授予,不需要用户确认。应用安装完成后就自动拥有这些权限,用户感知不到。比如:
ohos.permission.INTERNET— 网络访问ohos.permission.GET_NETWORK_INFO— 获取网络信息ohos.permission.SET_NETWORK_INFO— 设置网络信息ohos.permission.KEEP_BACKGROUND_RUNNING— 长时任务后台运行
这种权限你在module.json5里声明了就行,用户安装App后自动拥有,不需要任何额外操作。
user_grant权限
必须运行时动态申请,弹窗让用户确认。用户可以同意或拒绝,拒绝后功能不可用。比如:
ohos.permission.CAMERA— 相机ohos.permission.MICROPHONE— 麦克风ohos.permission.LOCATION— 精确定位ohos.permission.APPROXIMATELY_LOCATION— 大概位置ohos.permission.READ_IMAGEVIDEO— 读取图片/视频ohos.permission.WRITE_IMAGEVIDEO— 写入图片/视频ohos.permission.READ_CALENDAR— 读取日历ohos.permission.ANSWER_CALL— 接听电话
这种权限光在json5里声明是不够的,必须在代码里调用requestPermissionsFromUser,用户点了"允许"才算授权成功。如果你只是声明了但没有动态申请,调用相关API时会直接报权限错误——这就是我被拒审的原因。
我第一次做的时候,json5声明了CAMERA权限,代码里直接调相机API,结果运行时直接报权限错误。当时还以为json5配置有问题,检查了八百遍,最后才知道——user_grant权限必须动态申请!
怎么区分normal和user_grant
最简单的方法是看权限的授权方式。在HarmonyOS官方文档的权限列表里,每个权限都标注了授权方式。system_grant就是normal(系统自动授予),user_grant就是需要用户确认的。
经验法则:涉及用户隐私的权限都是user_grant——相机、麦克风、位置、通讯录、日历、图片等。不涉及隐私的都是normal——网络、震动、屏幕亮度等。不过也有例外,比如ohos.permission.DISTRIBUTED_DATASYNC(分布式数据同步)虽然是数据相关权限,但因为涉及跨设备数据传输,也是user_grant。
在开发过程中,你可以通过adb shell pm list permissions或者在DevEco Studio的权限管理界面查看所有权限的详细信息,包括授权方式。不过最靠谱的方法还是查官方文档——权限列表里每个权限都标注了授权方式。
还有一个很多人忽略的细节——user_grant权限的授权状态是会变的。用户可能在系统设置里随时撤销已经授予的权限。所以你每次使用敏感功能之前,都应该检查权限状态,不要缓存在内存里。我之前在aboutToAppear里检查了一次相机权限,然后整个页面生命周期内都认为权限是已授权的,结果用户中途去设置里关了相机权限,回到应用后调用相机API直接崩溃。正确的做法是每次使用功能前都检查权限,或者在onPageShow里重新检查。权限不是"申请一次管一辈子"的东西,用户随时可以收回。
module.json5配置
不管是normal还是user_grant,都需要在module.json5里声明:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
// normal权限,不需要reason和usedScene
},
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
// user_grant权限,必须提供reason和usedScene
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:microphone_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:location_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.LOCATION",
"reason": "$string:location_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
reason字符串:必须用$string资源引用——这是我被拒审的第二个原因
这是我被拒审的第二个原因,也是很多人容易忽略的。reason字段必须引用$string:资源,不能直接写中文原文。
// ❌ 直接写中文原文,审核不通过
{
"name": "ohos.permission.CAMERA",
"reason": "需要使用相机拍照",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
// 审核人员会看这个reason,直接写原文不符合规范
// ✅ 使用$string资源引用
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
然后在resources/base/element/string.json里定义这个字符串:
{
"string": [
{
"name": "camera_reason",
"value": "需要使用相机拍照以完成头像设置"
},
{
"name": "microphone_reason",
"value": "需要使用麦克风录音以发送语音消息"
},
{
"name": "location_reason",
"value": "需要获取您的位置信息以提供附近的商家推荐"
}
]
}
reason的内容也有讲究——不能太笼统。你写"需要使用相机"可能不够,得写清楚具体场景,比如"需要使用相机拍照以完成头像设置"。审核人员会看这个reason是否合理,是否跟应用功能匹配。如果你的App是个记账软件,reason写"需要使用相机拍照",审核人员会疑惑——记账为什么要用相机?你得写"需要使用相机扫描发票进行记账"才行。
usedScene字段
usedScene告诉系统这个权限在什么场景下使用:
- abilities — 使用该权限的Ability列表,通常是
["EntryAbility"]。如果你的应用有多个Ability都需要这个权限,就都列出来。 - when — 使用时机,
inuse(使用时)或always(始终)
大部分场景用inuse就行——只有在使用相关功能时才需要权限。always一般是后台持续需要权限的场景(比如后台定位、后台录音),审核会更严格,需要提供更详细的reason说明为什么需要后台持续使用。如果你的应用不是特别需要后台使用权限,千万不要写always,否则审核很可能被打回。
还有一个关于usedScene的细节——如果你声明了when: "inuse",但你的应用在后台也调用了需要权限的API,系统会拒绝。所以when的值必须跟你实际的使用场景匹配。
abilityAccessCtrl:权限管理核心API
创建AtManager实例
所有权限操作都通过AtManager实例来做:
import { abilityAccessCtrl, Permissions, bundleManager } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
// 创建权限管理器
let atManager = abilityAccessCtrl.createAtManager()
这里有个ArkTS的类型推断问题。如果写成let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(),类型标注虽然精确,但有时候会遇到命名空间引用的编译问题。我用下来直接让类型推断更省事:
// 直接让类型推断,简单省事
let atManager = abilityAccessCtrl.createAtManager()
// 或者显式声明类型(如果推断有问题或需要明确类型)
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager()
requestPermissionsFromUser:运行时申请权限
这是最核心的API——运行时向用户请求权限。调用后系统会弹出一个权限申请弹窗,用户可以选择允许或拒绝。
import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
@Entry
@Component
struct PermissionDemo {
@State permissionStatus: string = '未申请'
private async requestCameraPermission(): Promise<void> {
let atManager = abilityAccessCtrl.createAtManager()
let context = this.getUIContext().getHostContext() as common.UIAbilityContext
let permissions: Array<Permissions> = ['ohos.permission.CAMERA']
try {
let result = await atManager.requestPermissionsFromUser(context, permissions)
// result.authResults是一个数组,对应每个权限的授权结果
// 0 = 已授权(PERMISSION_GRANTED)
// -1 = 未授权(PERMISSION_DENIED)
let grantStatus = result.authResults[0]
if (grantStatus === 0) {
this.permissionStatus = '已授权'
// 继续执行需要相机权限的操作
this.openCamera()
} else {
this.permissionStatus = '用户拒绝'
// 用户拒绝了,引导用户去设置页面手动开启
this.showPermissionDeniedTip()
}
} catch (err) {
let e = err as BusinessError
console.error(`请求权限失败: ${e.code} - ${e.message}`)
this.permissionStatus = '请求失败'
}
}
build() {
Column({ space: 20 }) {
Text(`相机权限状态: ${this.permissionStatus}`)
.fontSize(16)
Button('请求相机权限')
.onClick(() => {
this.requestCameraPermission()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private openCamera(): void {
console.info('打开相机...')
}
private showPermissionDeniedTip(): void {
console.info('用户拒绝了权限,引导去设置')
}
}
authResults的值含义
requestPermissionsFromUser返回的PermissionRequestResult对象有个authResults数组,每个元素对应该权限的授权状态:
| 值 | 含义 | 对应常量 |
|---|---|---|
| 0 | 已授权 | PERMISSION_GRANTED |
| -1 | 未授权 | PERMISSION_DENIED |
这里有个重要细节——用户第一次被请求权限时,如果点了"拒绝",authResults返回-1。但系统还会给用户一次机会——下次请求时会再弹一次弹窗。如果用户点了"拒绝且不再询问",那以后请求就不再弹窗了,直接返回-1。
这意味着你不能简单地认为"返回-1就是用户拒绝了,下次还会弹窗"。你无法区分"用户拒绝了但下次还可以弹"和"用户拒绝了且不再询问"这两种情况。所以最好的做法是——如果用户拒绝了,就引导去系统设置页面手动开启权限。
let result = await atManager.requestPermissionsFromUser(context, permissions)
// 检查每个权限的状态
for (let i = 0; i < permissions.length; i++) {
let status = result.authResults[i]
if (status === 0) {
console.info(`${permissions[i]} 已授权`)
// 可以使用相关功能
} else {
console.info(`${permissions[i]} 未授权`)
// 引导用户去设置页面手动开启
}
}
checkAccessToken:检查当前权限状态
有时候你想先检查权限是否已授权,再决定是否弹出申请弹窗。比如用户进入相机页面前先检查相机权限,已授权就直接打开相机,未授权才弹出申请。这就要用到checkAccessToken。
获取tokenId
checkAccessToken需要两个参数:tokenId和权限名。tokenId需要从应用信息里获取——每个应用都有唯一的tokenId,用来标识应用身份。
import { bundleManager } from '@kit.AbilityKit'
// 获取本应用的bundleInfo(同步方法)
let bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
)
// 从bundleInfo中获取tokenId
let tokenId = bundleInfo.appInfo.accessTokenId
getBundleInfoForSelfSync是同步方法,不需要await。注意要传GET_BUNDLE_INFO_WITH_APPLICATION这个flag,不然appInfo可能是空的。
检查权限状态
private async checkCameraPermission(): Promise<boolean> {
let atManager = abilityAccessCtrl.createAtManager()
let bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
)
let tokenId = bundleInfo.appInfo.accessTokenId
let permission: Permissions = 'ohos.permission.CAMERA'
try {
let grantStatus = await atManager.checkAccessToken(tokenId, permission)
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
} catch (err) {
let e = err as BusinessError
console.error(`检查权限失败: ${e.code} - ${e.message}`)
return false
}
}
checkAccessToken返回GrantStatus枚举:
- GrantStatus.PERMISSION_GRANTED — 已授权
- GrantStatus.PERMISSION_DENIED — 未授权
同步版本:checkAccessTokenSync
如果不想用异步方式,还有个同步版本checkAccessTokenSync。在UI构建等不方便用await的场景下很好用:
private checkCameraPermissionSync(): boolean {
let atManager = abilityAccessCtrl.createAtManager()
let bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
)
let tokenId = bundleInfo.appInfo.accessTokenId
let grantStatus = atManager.checkAccessTokenSync(tokenId, 'ohos.permission.CAMERA')
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
在aboutToAppear里用同步版本检查权限状态,然后根据结果决定UI显示:
aboutToAppear(): void {
this.hasCameraPermission = this.checkCameraPermissionSync()
if (this.hasCameraPermission) {
// 已授权,可以直接使用相机
} else {
// 未授权,显示"请求权限"按钮
}
}
常见陷阱:先检查后申请的问题
我见过一种写法——先checkAccessToken检查权限,如果未授权再requestPermissionsFromUser。看起来逻辑没问题,但实际有个大坑:
第一次运行时,checkAccessToken返回DENIED(因为从未授权过),然后requestPermissionsFromUser弹出申请弹窗。这没问题。
但如果用户之前拒绝了权限(且选择了"不再询问"),checkAccessToken返回DENIED,requestPermissionsFromUser不再弹窗(直接返回DENIED),你的代码可能陷入逻辑错误——你以为是"弹了窗用户拒绝了",实际是"根本没弹窗直接拒绝了"。两种情况的处理方式应该不同——前者可以提示"您拒绝了权限",后者需要引导"去设置页面手动开启"。
// ❌ 有问题的逻辑:先检查后申请,无法区分"拒绝"和"拒绝且不再询问"
private async requestPermissionIfNeeded(): Promise<boolean> {
let hasPermission = await this.checkCameraPermission()
if (!hasPermission) {
// 如果用户之前点了"拒绝且不再询问",这里不会弹窗
// 直接返回-1,你的代码以为弹了窗被拒绝
let result = await this.requestPermissionsFromUser()
return result.authResults[0] === 0
}
return true
}
// ✅ 更好的做法:直接申请,或者不过度依赖"是否弹窗"的判断
private async requestPermissionSafely(): Promise<boolean> {
let atManager = abilityAccessCtrl.createAtManager()
let context = this.getUIContext().getHostContext() as common.UIAbilityContext
let permissions: Array<Permissions> = ['ohos.permission.CAMERA']
try {
let result = await atManager.requestPermissionsFromUser(context, permissions)
if (result.authResults[0] === 0) {
return true // 授权成功
}
// 不管是用户拒绝了还是不再弹窗,都引导去设置页面
this.showPermissionDeniedDialog()
return false
} catch (err) {
return false
}
}
最佳实践:try → fail → request → retry
这是业界推荐的权限申请模式——先尝试执行操作,如果因为权限不足而失败,再申请权限,然后重试。不要一进入页面就弹一堆权限申请弹窗,用户很烦这种。
private async takePhoto(): Promise<void> {
// 先尝试直接操作
try {
await this.doTakePhoto()
return // 成功了就不需要申请权限
} catch (err) {
let e = err as BusinessError
// 检查是否是权限不足导致的失败
if (e.code === 201) { // 权限不足的错误码
// 申请权限
let granted = await this.requestCameraPermission()
if (granted) {
// 权限获取成功,重试操作
try {
await this.doTakePhoto()
} catch (retryErr) {
// 重试也失败了,可能不是权限问题
this.showError('拍照失败')
}
} else {
// 用户拒绝权限,引导去设置
this.showPermissionDeniedTip()
}
} else {
// 其他错误,不是权限问题
this.showError('拍照失败')
}
}
}
这种模式的好处:
- 已授权的情况下不会弹窗,用户无感知
- 只在真正需要权限的时候才申请
- 逻辑清晰——失败→申请→重试
同时申请多个权限
如果你的应用需要多个权限,可以一次性申请,不用一个一个弹。比如视频通话场景需要同时申请相机和麦克风:
private async requestMultiplePermissions(): Promise<void> {
let atManager = abilityAccessCtrl.createAtManager()
let context = this.getUIContext().getHostContext() as common.UIAbilityContext
let permissions: Array<Permissions> = [
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE',
]
try {
let result = await atManager.requestPermissionsFromUser(context, permissions)
// 逐个检查每个权限的状态
for (let i = 0; i < permissions.length; i++) {
let status = result.authResults[i]
if (status === 0) {
console.info(`${permissions[i]} 已授权`)
} else {
console.info(`${permissions[i]} 未授权`)
}
}
} catch (err) {
let e = err as BusinessError
console.error(`申请权限失败: ${e.code}`)
}
}
多个权限一起申请时,系统会依次弹出每个权限的申请弹窗。用户可以逐个选择允许或拒绝。authResults数组里按顺序对应每个权限的结果。
不过我建议不要一次申请太多权限——三个以上用户就容易烦了,全部拒绝的概率大大增加。最好是按功能分批申请:进入相机页面申请CAMERA,进入录音页面申请MICROPHONE,进入地图页面申请LOCATION。每次最多申请1-2个相关联的权限。
定位权限的特殊情况
定位权限在HarmonyOS里分两个级别,这是很多开发者不知道的:
ohos.permission.APPROXIMATELY_LOCATION— 大概位置(城市级别精度,约5公里)ohos.permission.LOCATION— 精确位置(GPS级别精度,约10米)
这个设计跟Android类似——Android也是分粗略位置和精确定位。但HarmonyOS的弹窗行为有点不同。
如果你同时申请这两个权限,系统会弹出一个合并的定位权限弹窗,用户可以选择"大概位置"或"精确位置"。如果用户选了"大概位置",APPROXIMATELY_LOCATION授权,LOCATION不授权。
// 只申请大概位置——地图显示城市级别就够了
let permissions: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION']
// 申请精确位置——导航、打车需要精确位置
let permissions: Array<Permissions> = [
'ohos.permission.APPROXIMATELY_LOCATION',
'ohos.permission.LOCATION',
]
注意:申请LOCATION前必须先申请APPROXIMATELY_LOCATION。如果你只申请LOCATION不申请APPROXIMATELY_LOCATION,系统可能会拒绝。这是因为精确定位包含了大概位置的信息,所以大概位置是精确位置的前置条件。
// ❌ 只申请LOCATION,可能失败
let permissions: Array<Permissions> = ['ohos.permission.LOCATION']
// ✅ 同时申请两个
let permissions: Array<Permissions> = [
'ohos.permission.APPROXIMATELY_LOCATION',
'ohos.permission.LOCATION',
]
引导用户去设置页面
如果用户点了"拒绝且不再询问",requestPermissionsFromUser不会再弹窗了。这时候需要引导用户去系统设置页面手动开启权限。这是用户体验的关键——不要让用户卡在一个无法操作的状态。
import { common } from '@kit.AbilityKit'
private showPermissionDeniedDialog(): void {
AlertDialog.show({
title: '需要相机权限',
message: '您已拒绝相机权限,请在系统设置中手动开启,否则无法使用拍照功能',
primaryButton: {
value: '去设置',
action: () => {
this.openPermissionSettings()
}
},
secondaryButton: {
value: '取消',
action: () => {}
}
})
}
private openPermissionSettings(): void {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext
// 尝试打开应用详情页
let want = {
action: 'action.system.settings.application.info',
parameters: {
settingsParamBundleName: context.abilityInfo.bundleName,
},
}
context.startAbility(want)
}
不过打开系统设置页面的具体方式可能因系统版本不同而变化,建议测试时验证一下。有些版本用action.system.settings.application.info可以打开应用详情页,用户可以直接在那里开启权限。
实战:完整的权限管理工具类
我把权限相关的逻辑封装成了一个工具类,各个页面都可以复用。这样不用每个页面都写一堆权限申请的代码:
// PermissionUtil.ets
import { abilityAccessCtrl, bundleManager, Permissions, common } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
export class PermissionUtil {
// 检查单个权限(同步版本,适合在aboutToAppear中使用)
static checkPermission(permission: Permissions): boolean {
let atManager = abilityAccessCtrl.createAtManager()
let bundleInfo = bundleManager.getBundleInfoForSelfSync(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
)
let tokenId = bundleInfo.appInfo.accessTokenId
let grantStatus = atManager.checkAccessTokenSync(tokenId, permission)
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
// 请求单个权限
static async requestPermission(
context: common.UIAbilityContext,
permission: Permissions
): Promise<boolean> {
let atManager = abilityAccessCtrl.createAtManager()
let permissions: Array<Permissions> = [permission]
try {
let result = await atManager.requestPermissionsFromUser(context, permissions)
return result.authResults[0] === 0
} catch (err) {
let e = err as BusinessError
console.error(`请求权限失败: ${e.code} - ${e.message}`)
return false
}
}
// 请求多个权限
static async requestMultiplePermissions(
context: common.UIAbilityContext,
permissions: Array<Permissions>
): Promise<Map<Permissions, boolean>> {
let atManager = abilityAccessCtrl.createAtManager()
let resultMap = new Map<Permissions, boolean>()
try {
let result = await atManager.requestPermissionsFromUser(context, permissions)
for (let i = 0; i < permissions.length; i++) {
resultMap.set(permissions[i], result.authResults[i] === 0)
}
} catch (err) {
let e = err as BusinessError
console.error(`请求权限失败: ${e.code} - ${e.message}`)
}
return resultMap
}
// 检查并请求(便捷方法:先检查,未授权再申请)
static async ensurePermission(
context: common.UIAbilityContext,
permission: Permissions
): Promise<boolean> {
if (PermissionUtil.checkPermission(permission)) {
return true // 已经授权,不需要申请
}
return await PermissionUtil.requestPermission(context, permission)
}
}
在页面中使用这个工具类:
import { PermissionUtil } from '../utils/PermissionUtil'
import { common } from '@kit.AbilityKit'
@Entry
@Component
struct CameraPage {
@State hasCameraPermission: boolean = false
aboutToAppear(): void {
// 页面加载时检查权限状态
this.hasCameraPermission = PermissionUtil.checkPermission('ohos.permission.CAMERA')
}
build() {
Column({ space: 20 }) {
if (this.hasCameraPermission) {
// 已授权,显示相机功能
Text('相机权限已授权,可以使用相机功能')
.fontSize(16)
.fontColor('#333')
Button('拍照')
.onClick(() => {
this.takePhoto()
})
} else {
// 未授权,显示申请按钮
Text('相机权限未授权,点击下方按钮申请')
.fontSize(16)
.fontColor('#999')
Button('请求相机权限')
.onClick(async () => {
let context = this.getUIContext().getHostContext() as common.UIAbilityContext
this.hasCameraPermission = await PermissionUtil.requestPermission(
context,
'ohos.permission.CAMERA'
)
if (!this.hasCameraPermission) {
this.showGoToSettingsDialog()
}
})
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
private takePhoto(): void {
console.info('开始拍照...')
}
private showGoToSettingsDialog(): void {
AlertDialog.show({
title: '权限不足',
message: '请在系统设置中开启相机权限后重试',
primaryButton: {
value: '去设置',
action: () => {
// 打开设置页面
}
},
secondaryButton: {
value: '取消',
action: () => {}
}
})
}
}
ensurePermission这个便捷方法结合了"先检查再申请"的逻辑——如果已授权就直接返回true,如果未授权就弹窗申请。但它避免了前面说的"拒绝且不再询问"的陷阱——因为不管申请结果如何,都返回明确的boolean值,你可以根据结果决定后续操作。

常见踩坑汇总
坑1:只声明不申请
user_grant权限必须在代码中调用requestPermissionsFromUser- 光在json5里声明不够,运行时调用相关API会报权限错误
- 这是审核被拒最常见的原因
坑2:reason直接写中文
- 必须用
$string:资源引用 - 直接写中文原文审核不通过
- 我因为这个被拒过
坑3:reason写得太笼统
- "需要使用相机"太笼统
- "需要使用相机拍照以完成头像设置"才够具体
- reason要跟应用功能匹配,审核人员会看
坑4:先checkAccessToken再requestPermissionsFromUser
- 用户拒绝且不再询问后,request不再弹窗
- 你无法区分"弹了窗被拒绝"和"没弹窗直接拒绝"
- 建议:不管什么情况,拒绝就引导去设置
坑5:申请LOCATION但不申请APPROXIMATELY_LOCATION
- LOCATION依赖APPROXIMATELY_LOCATION
- 必须同时申请或先申请APPROXIMATELY_LOCATION
- 只申请LOCATION可能失败
坑6:一次申请太多权限
- 用户容易烦,全部拒绝的概率增加
- 按功能分批申请,最多3个一组
- 优先申请当前功能必须的权限
坑7:不处理用户拒绝的情况
- 用户拒绝后要给提示,引导去设置
- 不能什么都不做就卡在那
- 否则用户面对一个什么都不能操作的页面很困惑
坑8:忘记import Permissions类型
Permissions类型需要从@kit.AbilityKitimport- 不import的话
Array<Permissions>类型标注会报错 BusinessError需要从@kit.BasicServicesKitimport
坑9:checkAccessToken需要tokenId
- tokenId不是context本身,需要从bundleInfo获取
bundleManager.getBundleInfoForSelfSync要传GET_BUNDLE_INFO_WITH_APPLICATIONflag- 不传flag的话appInfo可能为空
坑10:requestPermissionsFromUser需要UIAbilityContext
- 不是所有context都行,需要UIAbilityContext
- 在Component中通过
this.getUIContext().getHostContext() as common.UIAbilityContext获取
权限管理这块虽然逻辑不复杂,但每个细节都跟审核挂钩。我第一版应用被拒了三次才搞对,每次被拒的理由都不一样但都是权限相关。建议大家提交审核前,把每个user_grant权限都跑一遍完整流程:声明→申请→授权→使用→拒绝→引导设置,确保每条路径都正常。不要像我一样,提交了才发现遗漏。
我还想强调一点——权限申请不仅仅是技术问题,更是产品设计问题。好的权限申请策略应该是"按需申请、合理说明、优雅降级"。"按需申请"就是只在用户用到功能时才申请权限,不要一启动就弹一堆。"合理说明"就是reason要写得清楚具体,让用户理解为什么需要这个权限。"优雅降级"就是用户拒绝权限后,应用不应该崩溃或卡死,而应该给出替代方案或者友好提示。
这三个原则做到了,审核基本没问题,用户体验也好。
实用要点回顾:
normal权限安装时自动授予,user_grant权限必须运行时申请requestPermissionsFromUser是运行时申请权限的核心APIauthResults数组:0=授权,-1=拒绝- json5的
reason必须用$string:资源引用,内容要具体 checkAccessToken/checkAccessTokenSync用于检查当前权限状态- 推荐try→fail→request→retry模式,不要一上来就申请权限
- 定位权限要先申请APPROXIMATELY_LOCATION再申请LOCATION
- 用户拒绝后引导去系统设置页面手动开启
- 分批申请权限,每次不超过3个
- 封装权限工具类便于复用
更多推荐


所有评论(0)