HarmonyOS开发:NEXT版权限模型——新权限体系
HarmonyOS开发:NEXT版权限模型——新权限体系
📌 核心要点:NEXT版权限模型从"安装时全量授权"变为"运行时按需申请+分级授权+用户可控撤回",权限粒度更细,申请流程更严,你的App必须重新设计权限策略。
背景与动机
你有没有遇到过这种情况——用户安装你的App,一看权限列表:位置、相机、麦克风、通讯录、存储……十几个权限一口气弹出来,用户直接点"拒绝"或者干脆卸载。
V5的权限模型有个大问题:很多敏感权限在安装时就自动授予了,用户根本不知道。等用户发现App在后台偷偷用位置、读通讯录的时候,信任已经崩了。
NEXT版彻底改了这套机制。核心变化就一句话:用户说了算。
什么意思?敏感权限必须运行时申请,用户可以逐个授权或拒绝,已经授权的权限随时可以撤回,而且App不能反复弹窗骚扰用户。这套机制对用户友好,但对开发者来说——你得重新设计整个权限申请流程。
核心原理
NEXT版权限分级体系
先搞清楚NEXT版的权限分几级,不同级别的申请策略完全不同:
graph TB
classDef normal fill:#2ecc71,stroke:#27ae60,color:#fff,stroke-width:2px
classDef system fill:#f39c12,stroke:#e67e22,color:#fff,stroke-width:2px
classDef user fill:#e74c3c,stroke:#c0392b,color:#fff,stroke-width:2px
classDef restricted fill:#9b59b6,stroke:#8e44ad,color:#fff,stroke-width:2px
A[NEXT权限分级] --> B[normal<br/>普通权限]:::normal
A --> C[system_basic<br/>系统基础权限]:::system
A --> D[user_grant<br/>用户授权权限]:::user
A --> E[restricted<br/>受限权限]:::restricted
B --> B1[安装时自动授予<br/>如:网络访问]:::normal
C --> C1[系统签名App才能申请<br/>如:系统设置修改]:::system
D --> D1[运行时弹窗申请<br/>如:相机、位置]:::user
E --> E1[仅系统App可用<br/>如:底层硬件访问]:::restricted
| 权限级别 | 授权方式 | 示例 | 能否撤回 |
|---|---|---|---|
| normal | 安装时自动授予 | INTERNET、GET_NETWORK_INFO | 不能 |
| system_basic | 系统签名App才能申请 | SET_TIME、CONNECTIVITY_INTERNAL | 不能 |
| user_grant | 运行时弹窗申请 | CAMERA、LOCATION、MICROPHONE | 可以 |
| restricted | 仅系统App | MANAGE_USB、INSTALL_BUNDLE | 不适用 |
和V5的核心区别在哪?
- V5的很多user_grant权限在安装时默认授予,NEXT全部改为运行时申请
- NEXT新增了权限使用说明——申请权限时必须告诉用户为什么需要这个权限
- NEXT支持权限的精确控制——比如位置权限可以只授权"大致位置"而非"精确位置"
- NEXT支持单次授权——用户可以选择"仅本次允许"
新增权限与废弃权限
| 变化类型 | 权限 | 说明 |
|---|---|---|
| 新增 | ohos.permission.APP_TRACKING_CONSENT |
跨应用追踪需要用户明确同意 |
| 新增 | ohos.permission.READ_IMAGEVIDEO |
替代旧的存储权限,精确到图片/视频 |
| 新增 | ohos.permission.READ_AUDIO |
精确到音频文件 |
| 新增 | ohos.permission.READ_DOCUMENT |
精确到文档文件 |
| 废弃 | ohos.permission.READ_MEDIA |
拆分为READ_IMAGEVIDEO/READ_AUDIO |
| 废弃 | ohos.permission.WRITE_MEDIA |
NEXT不再支持写媒体库,用安全保存 |
| 变更 | ohos.permission.LOCATION |
新增精确/大致位置选项 |
权限申请流程变化
V5和NEXT的权限申请流程对比:
graph LR
classDef v5 fill:#e74c3c,stroke:#c0392b,color:#fff,stroke-width:2px
classDef next fill:#2ecc71,stroke:#27ae60,color:#fff,stroke-width:2px
subgraph V5流程
V1[安装App]:::v5 --> V2[安装时自动<br/>授予部分权限]:::v5
V2 --> V3[运行时弹窗<br/>申请敏感权限]:::v5
V3 --> V4[用户选择<br/>允许/拒绝]:::v5
V4 --> V5[拒绝后可<br/>反复弹窗]:::v5
end
subgraph NEXT流程
N1[安装App]:::next --> N2[仅授予normal<br/>权限]:::next
N2 --> N3[使用功能时<br/>按需申请权限]:::next
N3 --> N4[展示权限<br/>使用说明]:::next
N4 --> N5[用户选择<br/>允许/仅本次/拒绝]:::next
N5 --> N6[拒绝后不可<br/>反复弹窗]:::next
N6 --> N7[引导用户到<br/>设置页面]:::next
end
关键差异:
- V5安装时自动授予部分权限 → NEXT只授予normal权限
- V5拒绝后可以反复弹窗 → NEXT拒绝后不能反复弹,只能引导到设置
- NEXT新增"仅本次允许"选项 → 用户可以临时授权,App退出后自动撤回
- NEXT新增权限使用说明 → 申请权限时必须展示reason
代码实战
基础用法:运行时权限申请
NEXT版的标准权限申请流程:
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
// 需要申请的权限列表
const REQ_PERMISSIONS: Permissions[] = [
'ohos.permission.CAMERA',
'ohos.permission.READ_IMAGEVIDEO'
];
@Entry
@Component
struct PermissionDemo {
@State cameraGranted: boolean = false;
@State mediaGranted: boolean = false;
async aboutToAppear() {
// 先检查已有权限
await this.check_permissions();
}
/**
* 检查权限状态
*/
async check_permissions(): Promise<void> {
const atManager = abilityAccessCtrl.createAtManager();
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
);
this.cameraGranted = await this.check_single(
atManager, bundleInfo.appInfo.accessTokenId, 'ohos.permission.CAMERA'
);
this.mediaGranted = await this.check_single(
atManager, bundleInfo.appInfo.accessTokenId, 'ohos.permission.READ_IMAGEVIDEO'
);
}
private async check_single(
atManager: abilityAccessCtrl.AtManager,
tokenId: number,
permission: Permissions
): Promise<boolean> {
try {
const status = await atManager.checkAccessToken(tokenId, permission);
return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch {
return false;
}
}
/**
* 申请权限——NEXT版标准流程
*/
async request_permissions(): Promise<void> {
const atManager = abilityAccessCtrl.createAtManager();
try {
// NEXT版:requestPermissionsFromUser会弹出系统权限对话框
// 对话框中会显示你在module.json5中配置的reason
const result = await atManager.requestPermissionsFromUser(
getContext(this),
REQ_PERMISSIONS
);
// 处理每个权限的授权结果
for (let i = 0; i < result.authResults.length; i++) {
const granted = result.authResults[i] === 0;
const perm = REQ_PERMISSIONS[i];
console.info(`权限 ${perm}: ${granted ? '已授权' : '被拒绝'}`);
if (perm === 'ohos.permission.CAMERA') {
this.cameraGranted = granted;
} else if (perm === 'ohos.permission.READ_IMAGEVIDEO') {
this.mediaGranted = granted;
}
}
} catch (err) {
console.error(`权限申请异常: ${JSON.stringify(err)}`);
}
}
build() {
Column() {
Text('权限状态')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Row() {
Text(`相机: ${this.cameraGranted ? '✅' : '❌'}`)
.fontSize(18)
Text(`媒体: ${this.mediaGranted ? '✅' : '❌'}`)
.fontSize(18)
.margin({ left: 20 })
}
.margin({ top: 20 })
Button('申请权限')
.margin({ top: 20 })
.onClick(() => this.request_permissions())
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
进阶用法:权限被拒后的处理
NEXT版权限被拒后不能反复弹窗,需要引导用户到设置页面手动开启:
import { abilityAccessCtrl, bundleManager, Permissions, common } from '@kit.AbilityKit';
/**
* NEXT版权限管理器
* 封装权限申请、拒绝处理、设置引导
*/
export class NextPermissionManager {
private context: common.UIAbilityContext;
private atManager: abilityAccessCtrl.AtManager;
// 记录权限被拒次数——用于判断是否需要引导到设置
private deniedCount: Map<string, number> = new Map();
constructor(context: common.UIAbilityContext) {
this.context = context;
this.atManager = abilityAccessCtrl.createAtManager();
}
/**
* 申请权限——带拒绝处理
*/
async request_with_fallback(
permissions: Permissions[],
reason: string
): Promise<boolean> {
// 1. 先检查是否已授权
const allGranted = await this.check_all(permissions);
if (allGranted) {
return true;
}
// 2. 申请权限
try {
const result = await this.atManager.requestPermissionsFromUser(
this.context, permissions
);
let allOk = true;
for (let i = 0; i < result.authResults.length; i++) {
const granted = result.authResults[i] === 0;
const perm = permissions[i];
if (!granted) {
allOk = false;
// 记录拒绝次数
const count = (this.deniedCount.get(perm) || 0) + 1;
this.deniedCount.set(perm, count);
console.warn(`权限 ${perm} 被拒绝 (第${count}次)`);
// NEXT版:拒绝2次以上,引导到设置页面
if (count >= 2) {
this.show_settings_dialog(perm, reason);
}
} else {
this.deniedCount.delete(perm); // 授权后清除拒绝记录
}
}
return allOk;
} catch (err) {
console.error(`权限申请异常: ${JSON.stringify(err)}`);
return false;
}
}
/**
* 引导用户到设置页面
*/
private show_settings_dialog(permission: string, reason: string): void {
// NEXT版:弹出AlertDialog引导用户去设置
// 注意:这里不能直接跳转设置,需要用户确认
console.info(`建议引导用户到设置页面开启权限: ${permission}`);
console.info(`权限用途: ${reason}`);
// 使用UIContext弹出对话框
// 实际项目中应该用AlertDialog
}
/**
* 跳转到应用设置页面
*/
async open_app_settings(): Promise<void> {
// NEXT版:通过隐式Want跳转到应用设置
const want = {
action: 'action.settings.app.info',
parameters: {
bundleName: this.context.abilityInfo.bundleName
}
};
try {
await this.context.startAbility(want);
} catch (err) {
console.error(`跳转设置失败: ${JSON.stringify(err)}`);
}
}
/**
* 检查所有权限是否已授权
*/
private async check_all(permissions: Permissions[]): Promise<boolean> {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
);
for (const perm of permissions) {
const status = await this.atManager.checkAccessToken(
bundleInfo.appInfo.accessTokenId, perm
);
if (status !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}
完整示例:按需权限申请框架
把权限申请、拒绝处理、使用说明整合成完整框架:
import { abilityAccessCtrl, bundleManager, Permissions, common } from '@kit.AbilityKit';
/**
* 权限配置项
*/
interface PermissionConfig {
permission: Permissions;
reason: string; // 权限使用说明——NEXT版必须提供
required: boolean; // 是否为必须权限(拒绝则功能不可用)
fallbackMessage: string; // 拒绝后的提示信息
}
/**
* NEXT版权限申请框架
* 按需申请、拒绝处理、设置引导一体化
*/
export class PermissionFramework {
private context: common.UIAbilityContext;
private atManager: abilityAccessCtrl.AtManager;
private configs: PermissionConfig[] = [];
constructor(context: common.UIAbilityContext) {
this.context = context;
this.atManager = abilityAccessCtrl.createAtManager();
}
/**
* 注册权限配置
*/
register(config: PermissionConfig): void {
this.configs.push(config);
}
/**
* 批量注册权限配置
*/
registerAll(configs: PermissionConfig[]): void {
configs.forEach(c => this.configs.push(c));
}
/**
* 按功能申请权限
* 只申请该功能需要的权限,不一次性申请所有权限
*/
async request_for_feature(featureName: string): Promise<boolean> {
// 找到该功能需要的权限
const featurePerms = this.configs.filter(c =>
c.reason.includes(featureName) || c.required
);
if (featurePerms.length === 0) {
console.warn(`未找到功能 ${featureName} 的权限配置`);
return true;
}
// 先检查已授权的
const needRequest: Permissions[] = [];
for (const config of featurePerms) {
const granted = await this.check_permission(config.permission);
if (!granted) {
needRequest.push(config.permission);
}
}
if (needRequest.length === 0) {
return true; // 所有权限已授权
}
// 申请缺失的权限
try {
const result = await this.atManager.requestPermissionsFromUser(
this.context, needRequest
);
let allGranted = true;
for (let i = 0; i < result.authResults.length; i++) {
const granted = result.authResults[i] === 0;
const perm = needRequest[i];
const config = featurePerms.find(c => c.permission === perm);
if (!granted && config?.required) {
allGranted = false;
console.warn(`必须权限 ${perm} 被拒绝: ${config.fallbackMessage}`);
} else if (!granted) {
console.info(`可选权限 ${perm} 被拒绝,功能可能受限`);
}
}
return allGranted;
} catch (err) {
console.error(`权限申请异常: ${JSON.stringify(err)}`);
return false;
}
}
/**
* 检查单个权限
*/
async check_permission(permission: Permissions): Promise<boolean> {
try {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
);
const status = await this.atManager.checkAccessToken(
bundleInfo.appInfo.accessTokenId, permission
);
return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
} catch {
return false;
}
}
/**
* 获取所有权限状态
*/
async get_all_status(): Promise<Map<string, boolean>> {
const statusMap = new Map<string, boolean>();
for (const config of this.configs) {
const granted = await this.check_permission(config.permission);
statusMap.set(config.permission, granted);
}
return statusMap;
}
}
// ===== 使用示例 =====
@Entry
@Component
struct PermissionFrameworkDemo {
private permFramework: PermissionFramework | null = null;
@State cameraReady: boolean = false;
@State locationReady: boolean = false;
aboutToAppear() {
this.permFramework = new PermissionFramework(
getContext(this) as common.UIAbilityContext
);
// 注册权限配置——NEXT版必须提供reason
this.permFramework.registerAll([
{
permission: 'ohos.permission.CAMERA',
reason: '拍照功能需要使用相机',
required: true,
fallbackMessage: '没有相机权限,拍照功能无法使用'
},
{
permission: 'ohos.permission.APP_TRACKING_CONSENT',
reason: '个性化推荐需要跨应用追踪',
required: false,
fallbackMessage: '没有追踪权限,推荐内容可能不够精准'
},
{
permission: 'ohos.permission.LOCATION',
reason: '附近功能需要获取您的位置',
required: false,
fallbackMessage: '没有位置权限,无法使用附近功能'
},
{
permission: 'ohos.permission.READ_IMAGEVIDEO',
reason: '选择图片功能需要读取相册',
required: true,
fallbackMessage: '没有相册权限,无法选择图片'
}
]);
}
// 点击拍照按钮时才申请相机权限
async onTakePhoto() {
if (!this.permFramework) return;
const granted = await this.permFramework.request_for_feature('拍照');
this.cameraReady = granted;
if (granted) {
console.info('可以开始拍照');
}
}
// 点击附近按钮时才申请位置权限
async onNearby() {
if (!this.permFramework) return;
const granted = await this.permFramework.request_for_feature('附近');
this.locationReady = granted;
if (granted) {
console.info('可以加载附近内容');
}
}
build() {
Column() {
Text('按需权限申请')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Button('📷 拍照')
.margin({ top: 20 })
.enabled(!this.cameraReady)
.onClick(() => this.onTakePhoto())
Button('📍 附近')
.margin({ top: 10 })
.enabled(!this.locationReady)
.onClick(() => this.onNearby())
if (this.cameraReady) {
Text('相机权限已就绪 ✅')
.fontSize(14)
.fontColor('#2ecc71')
.margin({ top: 10 })
}
if (this.locationReady) {
Text('位置权限已就绪 ✅')
.fontSize(14)
.fontColor('#2ecc71')
.margin({ top: 5 })
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
踩坑与注意事项
1. module.json5中必须声明reason
NEXT版要求所有user_grant权限在module.json5中声明reason,否则编译报错:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_reason", // 必填!
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse" // inuse(使用时)或 always(始终)
}
}
]
}
}
2. READ_MEDIA拆分为三个权限
V5的ohos.permission.READ_MEDIA在NEXT中拆分为:
ohos.permission.READ_IMAGEVIDEO——读取图片和视频ohos.permission.READ_AUDIO——读取音频ohos.permission.READ_DOCUMENT——读取文档
如果你只需要读取图片,不要申请READ_AUDIO和READ_DOCUMENT——按需申请。
3. 位置权限的精确/大致选项
NEXT版位置权限支持两种精度:
ohos.permission.LOCATION——精确位置(GPS级别)ohos.permission.APPROXIMATELY_LOCATION——大致位置(城市级别)
用户可以选择只授权大致位置。你的App需要处理"只有大致位置"的情况。
// 检查是否有精确位置
const hasExactLocation = await check_permission('ohos.permission.LOCATION');
const hasApproxLocation = await check_permission('ohos.permission.APPROXIMATELY_LOCATION');
if (hasExactLocation) {
// 使用精确位置
} else if (hasApproxLocation) {
// 使用大致位置——功能降级
console.warn('只有大致位置权限,定位精度受限');
} else {
// 没有位置权限
}
4. 权限被拒后不能反复弹窗
NEXT版限制了权限弹窗的频率。用户拒绝后,短时间内再次调用requestPermissionsFromUser不会弹出对话框,而是直接返回拒绝结果。
正确做法:拒绝后引导用户到设置页面手动开启,而不是反复弹窗。
5. 后台位置需要两个权限
如果你的App需要在后台获取位置,需要同时申请ohos.permission.LOCATION和ohos.permission.LOCATION_IN_BACKGROUND。而且后台位置权限的审核更严格——只有导航类、运动健康类App才能通过。
6. 单次授权的处理
NEXT新增了"仅本次允许"选项。用户选择后,App退出时权限自动撤回。你的App需要处理"权限突然消失"的情况——每次使用功能前都要检查权限,不能缓存权限状态。
HarmonyOS 6适配说明
HarmonyOS 6在NEXT权限模型的基础上,新增了以下特性:
- 权限使用审计:6.0新增权限使用记录,用户可以查看每个App何时使用了哪个权限
- 权限自动过期:6.0支持权限设置有效期,过期后自动撤回
- 最小权限推荐:6.0的DevEco Studio新增权限审查工具,自动检测过度申请的权限
- 隐私沙箱:6.0引入隐私沙箱机制,某些权限(如广告追踪)在沙箱中执行,App无法获取原始数据
升级到6.0后,建议关注权限使用审计的适配,以及隐私沙箱对广告和追踪功能的影响。
总结
NEXT版权限模型的核心变化:从"安装时全量授权"到"运行时按需申请"。
这对用户是好事——他们可以精确控制每个权限。但对开发者来说,你必须重新设计权限申请流程:不能用"一口气申请所有权限"的粗暴方式,而是要按功能、按场景、按需申请。
记住三条铁律:
- 按需申请——用到才申请,不提前申请
- 给理由——每个权限都要告诉用户为什么需要
- 优雅降级——权限被拒后功能降级,不能直接崩溃
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐ 流程不复杂但细节多 |
| 使用频率 | ⭐⭐⭐⭐⭐ 每个涉及敏感功能的App都要处理 |
| 重要程度 | ⭐⭐⭐⭐⭐ 权限处理不当,App直接被拒审 |
一句话:NEXT的权限模型是"用户说了算",你必须按需申请、给理由、优雅降级。
- 点赞
- 收藏
- 关注作者
评论(0)