HarmonyOS开发:权限测试——权限场景测试
HarmonyOS开发:权限测试——权限场景测试
📌 核心要点:权限是鸿蒙安全体系的核心防线——运行时权限申请、权限拒绝处理、权限撤销响应、ACL权限配置——任何一个环节处理不当,轻则功能异常,重则安全漏洞。权限测试就是验证你的应用在所有权限场景下的行为是否正确。
一、背景与动机
你有没有遇到过这种Bug?用户拒绝了相机权限,你的App直接崩了——因为代码里没处理权限拒绝的情况。或者更隐蔽的:用户授权了位置权限,用了一会儿又去设置里把权限关了,你的App还在用旧的位置数据,完全不知道权限已经没了。
权限问题有个特点——开发时很难发现,因为开发者通常都点了"允许"。你测了100遍,每次都允许了,所以一切正常。但用户第一次打开App,看到"是否允许访问相机",点了"拒绝"——然后你的App就炸了。
手动测试权限场景?你得去设置里来回开关权限,每改一次就跑一遍功能。权限多了(相机、位置、麦克风、存储、通讯录……),组合场景爆炸——你根本测不过来。权限测试就是用代码系统性地覆盖这些场景,确保无论用户怎么操作,你的App都能正确响应。
二、核心原理
2.1 HarmonyOS权限体系
flowchart TB
A[HarmonyOS权限体系] --> B[系统权限]
A --> C[自定义权限]
A --> D[ACL权限]
B --> B1[normal级权限<br/>安装时自动授予]
B --> B2[system_basic级权限<br/>需系统签名]
B --> B3[system_core级权限<br/>仅系统应用]
B --> B4[用户授权权限<br/>运行时弹窗申请]
B --> B5[受限开放权限<br/>需申请ACL]
C --> C1[自定义权限声明]
C --> C2[权限级别定义]
C --> C3[权限使用校验]
D --> D1[ACL配置]
D --> D2[权限审批流程]
D --> D3[受限权限使用]
classDef mainStyle fill:#4CAF50,stroke:#388E3C,color:#fff,font-weight:bold
classDef sysStyle fill:#3498DB,stroke:#2980B9,color:#fff
classDef customStyle fill:#2ECC71,stroke:#27AE60,color:#fff
classDef aclStyle fill:#F39C12,stroke:#E67E22,color:#fff
class A mainStyle
class B,B1,B2,B3,B4,B5 sysStyle
class C,C1,C2,C3 customStyle
class D,D1,D2,D3 aclStyle
2.2 权限测试场景矩阵
| 场景 | 用户操作 | 预期行为 | 常见Bug |
|---|---|---|---|
| 首次申请-允许 | 点击"允许" | 功能正常使用 | 无 |
| 首次申请-拒绝 | 点击"拒绝" | 优雅降级,提示用户 | 崩溃/功能异常 |
| 再次申请-允许 | 去设置手动开启 | 功能恢复正常 | 未监听权限变化 |
| 永久拒绝 | 勾选"不再询问"+拒绝 | 引导用户去设置页 | 反复弹窗/无引导 |
| 权限撤销 | 运行中在设置关闭权限 | 停止使用受保护功能 | 继续使用旧数据 |
| 权限恢复 | 运行中在设置开启权限 | 恢复功能 | 未监听权限恢复 |
| ACL权限缺失 | 未申请ACL | 安装失败或运行时报错 | 静默失败 |
2.3 权限API核心流程
import abilityAccessCtrl from '@ohos.abilityAccessCtrl'
import bundleManager from '@ohos.bundle.bundleManager'
// 检查权限状态
const atManager = abilityAccessCtrl.createAtManager()
const grantStatus = await atManager.checkAccessToken(
bundleManager.getBundleInfoForSelfSync().appId,
'ohos.permission.CAMERA'
)
// 申请权限
const result = await atManager.requestPermissionsFromUser(context, [
'ohos.permission.CAMERA',
'ohos.permission.LOCATION'
])
// result.authResults 数组,0=已授权,-1=未授权
三、代码实战
3.1 基础用法:运行时权限测试
// PermissionTestHelper.ets - 权限测试辅助工具
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'
import bundleManager from '@ohos.bundle.bundleManager'
import { Context } from '@ohos.abilityAccessCtrl'
export class PermissionTestHelper {
private atManager: abilityAccessCtrl.AtManager
private context: Context
constructor(context: Context) {
this.atManager = abilityAccessCtrl.createAtManager()
this.context = context
}
// 检查单个权限状态
async checkPermission(permission: Permissions): Promise<boolean> {
try {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
const grantStatus = await this.atManager.checkAccessToken(
bundleInfo.appId,
permission
)
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
} catch (e) {
return false
}
}
// 申请权限
async requestPermissions(permissions: Array<Permissions>): Promise<Array<number>> {
const result = await this.atManager.requestPermissionsFromUser(this.context, permissions)
return result.authResults
}
// 获取所有权限状态
async getAllPermissionStatus(permissions: Array<Permissions>): Promise<Record<string, boolean>> {
const status: Record<string, boolean> = {}
for (const permission of permissions) {
status[permission] = await this.checkPermission(permission)
}
return status
}
}
// ==================== 运行时权限测试 ====================
import { describe, it, beforeAll, assertEqual, assertTrue, assertFalse } from '@ohos/hypium'
import { PermissionTestHelper } from '../PermissionTestHelper'
export default function runtimePermissionTest() {
describe('运行时权限测试', () => {
let permHelper: PermissionTestHelper
beforeAll(() => {
permHelper = new PermissionTestHelper(getContext(this))
})
it('checkPermission_未申请时返回false', 0, async () => {
// 假设相机权限尚未申请
const granted = await permHelper.checkPermission('ohos.permission.CAMERA')
// 注意:这个断言取决于当前权限状态
// 在测试环境中,通常初始状态是未授权
assertFalse(granted, '未申请的权限应为false')
})
it('requestPermissions_申请相机权限', 0, async () => {
const results = await permHelper.requestPermissions(['ohos.permission.CAMERA'])
// 在自动化测试中,权限弹窗可能自动授权
// 验证返回结果格式正确
assertEqual(results.length, 1, '应返回1个权限的结果')
})
it('checkPermission_授权后返回true', 0, async () => {
// 先申请权限
await permHelper.requestPermissions(['ohos.permission.CAMERA'])
// 再检查权限
const granted = await permHelper.checkPermission('ohos.permission.CAMERA')
assertTrue(granted, '授权后权限应为true')
})
it('requestPermissions_批量申请多个权限', 0, async () => {
const results = await permHelper.requestPermissions([
'ohos.permission.CAMERA',
'ohos.permission.LOCATION',
'ohos.permission.MICROPHONE'
])
assertEqual(results.length, 3, '应返回3个权限的结果')
})
})
}
3.2 进阶用法:权限拒绝与撤销场景测试
// PermissionAwareService.ets - 权限感知的服务层
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'
export class PermissionAwareService {
private permissionCache: Map<string, boolean> = new Map()
// 执行需要权限的操作
async executeWithPermission(
permission: Permissions,
action: () => Promise<void>,
onDenied: () => void,
onError: (error: Error) => void
): Promise<void> {
try {
// 检查权限缓存
if (this.permissionCache.get(permission) === false) {
onDenied()
return
}
// 检查实际权限状态
const atManager = abilityAccessCtrl.createAtManager()
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
const grantStatus = await atManager.checkAccessToken(bundleInfo.appId, permission)
if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
this.permissionCache.set(permission, false)
onDenied()
return
}
this.permissionCache.set(permission, true)
await action()
} catch (e) {
onError(e as Error)
}
}
// 清除权限缓存(权限变化时调用)
clearCache(): void {
this.permissionCache.clear()
}
// 更新权限缓存
updatePermissionCache(permission: string, granted: boolean): void {
this.permissionCache.set(permission, granted)
}
}
// ==================== 权限拒绝场景测试 ====================
import { describe, it, beforeAll, beforeEach, assertEqual, assertTrue } from '@ohos/hypium'
import { PermissionAwareService } from '../PermissionAwareService'
export default function permissionDeniedTest() {
describe('权限拒绝场景测试', () => {
let service: PermissionAwareService
let lastAction: string = ''
let lastError: string = ''
beforeAll(() => {
service = new PermissionAwareService()
})
beforeEach(() => {
service.clearCache()
lastAction = ''
lastError = ''
})
it('权限被拒绝_执行降级逻辑', 0, async () => {
// 模拟权限被拒绝(缓存设为false)
service.updatePermissionCache('ohos.permission.CAMERA', false)
await service.executeWithPermission(
'ohos.permission.CAMERA',
async () => { lastAction = '拍照' },
() => { lastAction = '提示用户授权' },
(e) => { lastError = e.message }
)
assertEqual(lastAction, '提示用户授权', '权限拒绝时应执行降级逻辑')
})
it('权限被撤销_停止受保护操作', 0, async () => {
// 先模拟有权限
service.updatePermissionCache('ohos.permission.LOCATION', true)
await service.executeWithPermission(
'ohos.permission.LOCATION',
async () => { lastAction = '获取位置' },
() => { lastAction = '提示用户授权' },
(e) => { lastError = e.message }
)
assertEqual(lastAction, '获取位置', '有权限时应正常执行')
// 模拟权限被撤销
service.updatePermissionCache('ohos.permission.LOCATION', false)
lastAction = ''
await service.executeWithPermission(
'ohos.permission.LOCATION',
async () => { lastAction = '获取位置' },
() => { lastAction = '提示用户授权' },
(e) => { lastError = e.message }
)
assertEqual(lastAction, '提示用户授权', '权限撤销后应执行降级逻辑')
})
it('权限恢复_自动恢复正常功能', 0, async () => {
// 模拟权限从无到有
service.updatePermissionCache('ohos.permission.MICROPHONE', false)
await service.executeWithPermission(
'ohos.permission.MICROPHONE',
async () => { lastAction = '录音' },
() => { lastAction = '提示用户授权' },
(e) => { lastError = e.message }
)
assertEqual(lastAction, '提示用户授权', '无权限时应降级')
// 模拟权限恢复
service.clearCache() // 清除缓存,重新检查
service.updatePermissionCache('ohos.permission.MICROPHONE', true)
lastAction = ''
await service.executeWithPermission(
'ohos.permission.MICROPHONE',
async () => { lastAction = '录音' },
() => { lastAction = '提示用户授权' },
(e) => { lastError = e.message }
)
assertEqual(lastAction, '录音', '权限恢复后应正常执行')
})
it('多个权限_部分拒绝部分允许', 0, async () => {
// 相机允许,位置拒绝
service.updatePermissionCache('ohos.permission.CAMERA', true)
service.updatePermissionCache('ohos.permission.LOCATION', false)
let cameraResult = ''
let locationResult = ''
await service.executeWithPermission(
'ohos.permission.CAMERA',
async () => { cameraResult = '正常' },
() => { cameraResult = '降级' },
(e) => { cameraResult = '错误' }
)
await service.executeWithPermission(
'ohos.permission.LOCATION',
async () => { locationResult = '正常' },
() => { locationResult = '降级' },
(e) => { locationResult = '错误' }
)
assertEqual(cameraResult, '正常', '相机权限允许应正常')
assertEqual(locationResult, '降级', '位置权限拒绝应降级')
})
})
}
3.3 完整示例:ACL权限测试
// AclPermissionConfig.ets - ACL权限配置验证器
export interface AclPermissionCheck {
permission: string
reason: string // 使用该权限的原因
aclRequired: boolean // 是否需要ACL
aclApplied: boolean // 是否已申请ACL
usedInCode: boolean // 代码中是否实际使用
}
export class AclPermissionValidator {
// 检查module.json5中的权限配置
static validateAclPermissions(
declaredPermissions: Array<string>, // module.json5中声明的权限
aclPermissions: Array<string>, // 已获得ACL的权限
codeUsedPermissions: Array<string> // 代码中实际使用的权限
): Array<AclPermissionCheck> {
// 受限开放权限列表(需要ACL)
const restrictedPermissions = [
'ohos.permission.READ_CONTACTS',
'ohos.permission.WRITE_CONTACTS',
'ohos.permission.READ_CALENDAR',
'ohos.permission.WRITE_CALENDAR',
'ohos.permission.READ_CALL_LOG',
'ohos.permission.WRITE_CALL_LOG',
'ohos.permission.READ_MESSAGES',
'ohos.permission.SEND_MESSAGES',
'ohos.permission.APPROXIMATELY_LOCATION',
'ohos.permission.LOCATION',
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE'
]
const results: Array<AclPermissionCheck> = []
for (const permission of declaredPermissions) {
const isRestricted = restrictedPermissions.includes(permission)
const hasAcl = aclPermissions.includes(permission)
const isUsed = codeUsedPermissions.includes(permission)
results.push({
permission,
reason: isRestricted ? '受限开放权限,需要ACL审批' : '普通用户授权权限',
aclRequired: isRestricted,
aclApplied: hasAcl,
usedInCode: isUsed
})
}
return results
}
}
// ==================== ACL权限测试 ====================
import { describe, it, assertEqual, assertTrue } from '@ohos/hypium'
import { AclPermissionValidator, AclPermissionCheck } from '../AclPermissionValidator'
export default function aclPermissionTest() {
describe('ACL权限测试', () => {
it('受限权限_未申请ACL应标记为缺失', 0, () => {
const declaredPermissions = [
'ohos.permission.INTERNET', // normal级,不需要ACL
'ohos.permission.CAMERA', // 受限开放,需要ACL
'ohos.permission.LOCATION' // 受限开放,需要ACL
]
const aclPermissions = [
'ohos.permission.CAMERA' // 只申请了相机的ACL
]
const codeUsedPermissions = [
'ohos.permission.INTERNET',
'ohos.permission.CAMERA',
'ohos.permission.LOCATION'
]
const results = AclPermissionValidator.validateAclPermissions(
declaredPermissions, aclPermissions, codeUsedPermissions
)
// 验证INTERNET不需要ACL
const internetCheck = results.find(r => r.permission === 'ohos.permission.INTERNET')
assertEqual(internetCheck?.aclRequired, false, 'INTERNET不需要ACL')
// 验证CAMERA已申请ACL
const cameraCheck = results.find(r => r.permission === 'ohos.permission.CAMERA')
assertEqual(cameraCheck?.aclRequired, true, 'CAMERA需要ACL')
assertEqual(cameraCheck?.aclApplied, true, 'CAMERA已申请ACL')
// 验证LOCATION未申请ACL(问题!)
const locationCheck = results.find(r => r.permission === 'ohos.permission.LOCATION')
assertEqual(locationCheck?.aclRequired, true, 'LOCATION需要ACL')
assertEqual(locationCheck?.aclApplied, false, 'LOCATION未申请ACL——这是配置缺陷')
})
it('声明了但未使用的权限_应标记为冗余', 0, () => {
const declaredPermissions = [
'ohos.permission.INTERNET',
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE' // 声明了但没用
]
const aclPermissions: Array<string> = []
const codeUsedPermissions = [
'ohos.permission.INTERNET',
'ohos.permission.CAMERA'
]
const results = AclPermissionValidator.validateAclPermissions(
declaredPermissions, aclPermissions, codeUsedPermissions
)
const micCheck = results.find(r => r.permission === 'ohos.permission.MICROPHONE')
assertEqual(micCheck?.usedInCode, false, 'MICROPHONE声明了但未使用——冗余权限')
})
it('代码中使用了但未声明的权限_应标记为缺失', 0, () => {
const declaredPermissions = [
'ohos.permission.INTERNET'
]
const aclPermissions: Array<string> = []
const codeUsedPermissions = [
'ohos.permission.INTERNET',
'ohos.permission.CAMERA' // 代码中用了但没声明
]
const results = AclPermissionValidator.validateAclPermissions(
declaredPermissions, aclPermissions, codeUsedPermissions
)
// CAMERA不在声明列表中,所以results里不会有它
// 但代码中使用了它——这是一个配置缺陷
const cameraDeclared = results.find(r => r.permission === 'ohos.permission.CAMERA')
assertEqual(cameraDeclared, undefined, 'CAMERA未在module.json5中声明但代码中使用了——配置缺失')
})
it('完整权限配置验证_生成报告', 0, () => {
const declaredPermissions = [
'ohos.permission.INTERNET',
'ohos.permission.CAMERA',
'ohos.permission.LOCATION',
'ohos.permission.MICROPHONE'
]
const aclPermissions = [
'ohos.permission.CAMERA',
'ohos.permission.LOCATION'
]
const codeUsedPermissions = [
'ohos.permission.INTERNET',
'ohos.permission.CAMERA',
'ohos.permission.LOCATION'
]
const results = AclPermissionValidator.validateAclPermissions(
declaredPermissions, aclPermissions, codeUsedPermissions
)
// 统计问题
const missingAcl = results.filter(r => r.aclRequired && !r.aclApplied)
const redundant = results.filter(r => !r.usedInCode)
const allGood = missingAcl.length === 0 && redundant.length === 0
assertTrue(missingAcl.length === 0, `缺少ACL的权限: ${missingAcl.map(r => r.permission).join(', ')}`)
assertTrue(redundant.length <= 1, `冗余权限: ${redundant.map(r => r.permission).join(', ')}`)
})
})
}
四、踩坑与注意事项
坑点1:权限申请时机不对
用户刚打开App,还没看到任何内容,就弹出一堆权限申请——“允许访问相机?”“允许访问位置?”“允许访问麦克风?”——用户一脸懵,全点了拒绝。
建议:权限申请遵循"用的时候再申请"原则。用户点拍照按钮时才申请相机权限,点定位按钮时才申请位置权限。首次启动只申请必须的权限(如网络权限)。
坑点2:权限拒绝后反复弹窗
用户拒绝了相机权限,你每次打开拍照页面都弹一次权限申请——用户烦了,直接卸载。
建议:检测到用户选择"拒绝且不再询问"后,不再弹窗,而是显示一个说明页面,解释为什么需要这个权限,并提供"去设置"按钮让用户手动开启。
坑点3:权限缓存导致状态不一致
你缓存了权限状态,用户在设置里改了权限,你的缓存还是旧的——代码以为有权限,实际已经没了。
建议:权限缓存的生命周期要短。在关键操作前重新检查权限状态,不要完全依赖缓存。或者在onPageShow/onWindowStageCreate等生命周期回调中刷新缓存。
坑点4:module.json5权限声明和代码不同步
你在module.json5里声明了5个权限,但代码里只用了3个——多声明的2个权限是安全风险。反过来,代码里用了5个权限,但只声明了3个——运行时直接报错。
建议:写一个构建脚本,自动扫描代码中的权限使用,和module.json5中的声明做对比,不一致就报错。
坑点5:ACL权限审批周期长
受限开放权限需要向华为申请ACL,审批可能要几天。如果你在项目后期才发现需要ACL,发布时间就得推迟。
建议:项目初期就梳理所有需要的权限,特别是受限开放权限,尽早申请ACL。把权限清单作为需求评审的一部分。
坑点6:多用户/多设备权限不同步
平板上授权了相机权限,手机上没授权——同一个账号,不同设备的权限状态是独立的。如果你的逻辑假设"授权过就永远有权限",在多设备场景下会出问题。
建议:不要假设权限状态,每次使用前都检查。
坑点7:测试环境中权限弹窗行为不同
真机上权限弹窗有"允许/拒绝/拒绝且不再询问"三个选项,模拟器上可能只有"允许/拒绝"。自动化测试中权限弹窗可能自动授权,导致你测不到拒绝场景。
建议:权限拒绝场景的测试用Mock权限状态,而不是依赖真实的权限弹窗。在PermissionAwareService中注入权限状态,可以模拟任何场景。
五、HarmonyOS 6适配说明
API差异表
| 功能/接口 | HarmonyOS 5 | HarmonyOS 6 | 变更说明 |
|---|---|---|---|
| 权限申请 | requestPermissionsFromUser | requestPermissionsFromUser | 新增权限说明回调 |
| 权限检查 | checkAccessToken | checkAccessToken | 新增批量检查API |
| 权限监听 | 无 | onPermissionChange | 权限变更实时通知 |
| ACL管理 | 手动配置 | @ohos.aclManager | ACL自动校验 |
| 权限测试 | 手动模拟 | @PermissionTest装饰器 | 自动权限场景测试 |
行为变更
-
权限说明回调:申请权限时必须提供权限使用说明(reason),系统会在弹窗中展示。如果reason为空,权限申请直接失败。
-
onPermissionChange实时监听:HarmonyOS 6支持监听权限状态变化,用户在设置中修改权限后,应用能实时收到通知,不再需要手动轮询检查。
-
@PermissionTest装饰器:测试框架新增权限场景装饰器,可以自动模拟权限授予、拒绝、撤销等场景,不需要手动Mock。
适配代码
// HarmonyOS 6权限测试
import { describe, it, assertEqual, assertTrue } from '@ohos/hypium'
import { PermissionTest, PermissionScenario } from '@ohos/hypium'
export default function harmonyOS6PermissionTest() {
describe('HarmonyOS 6权限测试', () => {
// 自动模拟权限拒绝场景
@PermissionTest({
permission: 'ohos.permission.CAMERA',
scenario: PermissionScenario.DENIED
})
it('相机权限被拒绝_优雅降级', 0, async () => {
const result = await cameraService.takePhoto()
assertTrue(!result.success, '权限拒绝时拍照应失败')
assertEqual(result.error, 'PERMISSION_DENIED', '错误码应为PERMISSION_DENIED')
})
// 自动模拟权限撤销场景
@PermissionTest({
permission: 'ohos.permission.LOCATION',
scenario: PermissionScenario.REVOKED
})
it('位置权限被撤销_停止定位', 0, async () => {
const result = await locationService.getCurrentLocation()
assertTrue(!result.success, '权限撤销时定位应失败')
})
// 权限变更实时监听
it('onPermissionChange_权限变更通知', 0, async () => {
let permissionChanged = false
const atManager = abilityAccessCtrl.createAtManager()
atManager.onPermissionChange(bundleInfo.appId, ['ohos.permission.CAMERA'], (changeList) => {
permissionChanged = true
assertEqual(changeList[0].permission, 'ohos.permission.CAMERA')
})
// 模拟权限变更...
assertTrue(permissionChanged, '权限变更应触发回调')
})
})
}
六、总结
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ |
| 使用频率 | ⭐⭐⭐⭐ |
| 重要程度 | ⭐⭐⭐⭐⭐ |
权限测试的核心就一句话:永远不要假设用户会点"允许"。你的代码必须在权限被拒绝、被撤销、被恢复的每一种场景下都能正确响应。权限申请时机要合理(用的时候再申请),拒绝处理要优雅(降级而不是崩溃),撤销响应要及时(停止使用受保护功能)。ACL权限是鸿蒙特有的安全机制,受限开放权限必须提前申请ACL,否则上线前才发现就晚了。权限测试不是可选项,是安全合规的硬性要求。
- 点赞
- 收藏
- 关注作者
评论(0)