HarmonyOS APP声明、校验与跨应用权限安全实践
HarmonyOS APP声明、校验与跨应用权限安全实践
📌 核心要点:自定义权限让应用可以定义自己的权限体系,实现跨应用访问控制、ExtensionAbility 保护、组件级权限校验等高级安全能力。但自定义权限涉及等级配置、跨应用校验、安全考量等多个维度,稍有不慎就会留下安全漏洞。
一、背景与动机
想象你是一家连锁酒店的老板。酒店有公共区域(大堂、走廊),任何住客都能自由进出。但有些区域是受限的——比如行政酒廊只对白金会员开放,员工更衣室只对工作人员开放,机房更是只有 IT 部门才能进入。
为了管理这些不同级别的访问权限,你不能只靠"一刀切"的门禁系统,而是需要自定义不同级别的门禁卡——白金卡、员工卡、IT卡,每种卡对应不同的通行范围。
鸿蒙的自定义权限机制,就是让应用拥有这种"自定义门禁"的能力。系统预定义的权限(如相机、位置)覆盖的是通用场景,但当你需要:
- 保护自己的 ExtensionAbility,只允许特定应用调用
- 实现跨应用的数据访问控制,A 应用只能被 B 应用访问
- 为组件设置访问门槛,不是谁都能启动你的 Service
这时候,自定义权限就派上用场了。
但要注意:自定义权限是一把双刃剑。用好了,你的应用固若金汤;用不好,可能留下安全漏洞,让恶意应用有机可乘。所以,理解自定义权限的安全模型至关重要。
二、核心原理
2.1 自定义权限的声明与校验流程
flowchart TD
A[应用A声明自定义权限] --> B[在 module.json5 的 definePermissions 中定义]
B --> C[系统注册该权限]
D[应用B声明使用该权限] --> E[在 module.json5 的 requestPermissions 中申请]
E --> F{应用B的APL >= 权限的APL?}
F -->|是| G[安装时授权 / 运行时请求]
F -->|否| H[权限申请失败]
G --> I[应用B调用应用A的受保护组件]
I --> J[应用A校验调用者的权限]
J --> K{调用者拥有自定义权限?}
K -->|是| L[允许访问 ✅]
K -->|否| M[拒绝访问 ❌]
classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
classDef error fill:#F44336,stroke:#D32F2F,color:#fff
classDef info fill:#2196F3,stroke:#1976D2,color:#fff
classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
class A,B,C,D,E,G,I,L primary
class F,K purple
class H,M error
class J warning
2.2 自定义权限声明字段详解
在 module.json5 中,通过 definePermissions 数组声明自定义权限:
{
"module": {
"definePermissions": [
{
"name": "com.example.myapp.MY_PERMISSION",
"grantMode": "system_grant",
"availableLevel": "system_basic",
"label": "$string:my_permission_label",
"description": "$string:my_permission_desc"
}
]
}
}
各字段含义:
| 字段 | 必填 | 说明 |
|---|---|---|
| name | 是 | 权限名称,建议使用 包名.权限名 格式,确保全局唯一 |
| grantMode | 是 | 授权方式:system_grant(系统自动授权)或 user_grant(用户手动授权) |
| availableLevel | 是 | 可使用的APL级别:normal/system_basic/system_core |
| label | 否 | 权限的简短标签,用于UI展示 |
| description | 否 | 权限的详细描述 |
2.3 自定义权限的命名规范
权限名称必须全局唯一,建议遵循以下命名规范:
<包名>.<权限类别>.<具体权限名>
示例:
com.example.myapp.DATA.READ → 读取数据权限
com.example.myapp.SERVICE.ACCESS → 访问服务权限
com.example.myapp.ADMIN.CONTROL → 管理控制权限
为什么要用包名作为前缀? 因为权限名称是在整个系统中注册的,如果两个应用都定义了 MY_PERMISSION,就会冲突。加上包名前缀,就像域名一样,确保不会撞名。
2.4 APL 级别与自定义权限的关系
自定义权限的 availableLevel 决定了哪些应用可以申请该权限:
| availableLevel | 谁能申请 | 典型场景 |
|---|---|---|
| normal | 任何应用 | 基础数据访问、简单服务调用 |
| system_basic | 系统签名应用 | 敏感数据访问、核心服务调用 |
| system_core | 仅系统核心应用 | 系统级管理、底层控制 |
安全原则:availableLevel 应该设为满足需求的最低级别。如果你的权限只是保护一个普通数据接口,设为 normal 就够了,没必要设为 system_basic。
2.5 跨应用权限校验机制
当应用 A 定义了自定义权限,应用 B 想要访问 A 的受保护组件时:
- 应用 A 在
definePermissions中定义权限,在组件的permissions字段中引用该权限 - 应用 B 在
requestPermissions中申请该权限 - 系统 在 B 安装时或运行时授予该权限
- 应用 A 在组件被调用时,系统自动校验调用者是否拥有该权限
三、代码实战
示例1:自定义权限声明与 ExtensionAbility 保护
这个示例展示如何定义自定义权限,并用它保护一个 ServiceExtensionAbility:
// module.json5 - 应用A的模块配置(权限定义方)
{
"module": {
"name": "serviceprovider",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "MainAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
// ====== 自定义权限声明 ======
"definePermissions": [
{
// 数据读取权限 - 允许其他应用读取本应用的数据
"name": "com.example.serviceprovider.DATA_READ",
"grantMode": "system_grant",
"availableLevel": "normal",
"label": "$string:data_read_label",
"description": "$string:data_read_desc"
},
{
// 数据写入权限 - 允许其他应用写入数据
"name": "com.example.serviceprovider.DATA_WRITE",
"grantMode": "system_grant",
"availableLevel": "normal",
"label": "$string:data_write_label",
"description": "$string:data_write_desc"
},
{
// 管理权限 - 仅系统应用可申请
"name": "com.example.serviceprovider.ADMIN_CONTROL",
"grantMode": "system_grant",
"availableLevel": "system_basic",
"label": "$string:admin_control_label",
"description": "$string:admin_control_desc"
}
],
// ====== 受保护的 ExtensionAbility ======
"extensionAbilities": [
{
"name": "DataServiceExtension",
"srcEntry": "./ets/extension/DataServiceExtension.ets",
"type": "service",
"description": "$string:data_service_desc",
"exported": true,
// 关键:通过 permissions 字段保护此 Extension
// 只有拥有指定权限的调用者才能连接此服务
"permissions": [
"com.example.serviceprovider.DATA_READ"
]
},
{
"name": "AdminServiceExtension",
"srcEntry": "./ets/extension/AdminServiceExtension.ets",
"type": "service",
"description": "$string:admin_service_desc",
"exported": true,
// 管理服务需要更高级别的权限
"permissions": [
"com.example.serviceprovider.ADMIN_CONTROL"
]
}
],
"abilities": [
{
"name": "MainAbility",
"srcEntry": "./ets/ability/MainAbility.ets",
"description": "$string:main_ability_desc",
"icon": "$media:icon",
"label": "$string:main_ability_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
]
}
}
对应的字符串资源:
// base/element/string.json
{
"string": [
{ "name": "data_read_label", "value": "数据读取权限" },
{ "name": "data_read_desc", "value": "允许应用读取服务提供者的数据" },
{ "name": "data_write_label", "value": "数据写入权限" },
{ "name": "data_write_desc", "value": "允许应用向服务提供者写入数据" },
{ "name": "admin_control_label", "value": "管理控制权限" },
{ "name": "admin_control_desc", "value": "允许应用管理服务提供者的核心配置" }
]
}
ExtensionAbility 的实现:
// extension/DataServiceExtension.ets - 受保护的数据服务
import { ServiceExtensionAbility } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
// 数据存储(模拟)
const dataStore: Map<string, string> = new Map();
dataStore.set('config_version', '1.0.0');
dataStore.set('server_url', 'https://api.example.com');
dataStore.set('last_sync_time', '2025-01-15T10:30:00Z');
export default class DataServiceExtension extends ServiceExtensionAbility {
// 服务创建
onCreate(want): void {
console.info('[DataService] 服务创建');
// 此时调用者已经通过权限校验,可以安全地提供服务
}
// 处理客户端连接请求
onConnect(want): rpc.RemoteObject {
console.info('[DataService] 客户端连接');
// 返回数据服务的远程对象
return new DataServiceRemoteObject('DataServiceStub');
}
// 服务销毁
onDestroy(): void {
console.info('[DataService] 服务销毁');
}
}
// 数据服务远程对象实现
class DataServiceRemoteObject extends rpc.RemoteObject {
constructor(descriptor: string) {
super(descriptor);
}
// 处理远程请求
onRemoteRequest(code: number, data: rpc.MessageSequence, reply: rpc.MessageSequence, option: rpc.MessageOption): boolean {
switch (code) {
case 1: // 读取数据
const key = data.readString();
const value = dataStore.get(key) || '';
reply.writeString(value);
console.info(`[DataService] 读取数据: ${key} = ${value}`);
return true;
case 2: // 写入数据
const writeKey = data.readString();
const writeValue = data.readString();
dataStore.set(writeKey, writeValue);
reply.writeInt(0); // 0表示成功
console.info(`[DataService] 写入数据: ${writeKey} = ${writeValue}`);
return true;
case 3: // 获取所有数据键
const keys = Array.from(dataStore.keys());
reply.writeInt(keys.length);
for (const k of keys) {
reply.writeString(k);
}
return true;
default:
console.error(`[DataService] 未知请求码: ${code}`);
return false;
}
}
}
示例2:调用方申请自定义权限并连接服务
应用 B 需要申请应用 A 定义的自定义权限,然后连接受保护的 ExtensionAbility:
// module.json5 - 应用B的模块配置(权限申请方)
{
"module": {
"name": "serviceconsumer",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "MainAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
// ====== 申请自定义权限 ======
"requestPermissions": [
{
// 申请应用A定义的数据读取权限
"name": "com.example.serviceprovider.DATA_READ",
"reason": "$string:consume_data_reason"
}
],
"abilities": [
{
"name": "MainAbility",
"srcEntry": "./ets/ability/MainAbility.ets",
"description": "$string:main_ability_desc",
"icon": "$media:icon",
"label": "$string:main_ability_label",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
]
}
}
连接服务的代码实现:
// pages/ServiceConnectPage.ets - 连接受保护服务的页面
import { common, Want, AbilityConstant } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct ServiceConnectPage {
// 连接状态
@State connectionStatus: string = '未连接';
// 远程对象
private remoteObject: rpc.RemoteObject | null = null;
// 连接ID
private connectionId: number = -1;
// 读取到的数据
@State readData: string = '';
// 数据键列表
@State dataKeys: string[] = [];
private getContext(): common.UIAbilityContext {
return this.getUIContext().getHostContext() as common.UIAbilityContext;
}
build() {
Scroll() {
Column() {
Text('自定义权限 - 服务调用方')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 40, bottom: 20 })
// 连接状态
Row() {
Circle({ width: 12, height: 12 })
.fill(this.connectionStatus.includes('已连接') ? '#4CAF50' : '#FF9800')
.margin({ right: 8 })
Text(this.connectionStatus)
.fontSize(16)
}
.margin({ bottom: 20 })
// 操作按钮组
Button('🔗 连接数据服务')
.width('80%')
.height(48)
.fontSize(16)
.backgroundColor('#4CAF50')
.fontColor(Color.White)
.borderRadius(24)
.margin({ bottom: 12 })
.onClick(() => {
this.connectToService();
})
Button('📋 获取所有数据键')
.width('80%')
.height(48)
.fontSize(16)
.backgroundColor('#2196F3')
.fontColor(Color.White)
.borderRadius(24)
.margin({ bottom: 12 })
.enabled(this.remoteObject !== null)
.onClick(() => {
this.getAllKeys();
})
Button('📖 读取数据')
.width('80%')
.height(48)
.fontSize(16)
.backgroundColor('#FF9800')
.fontColor(Color.White)
.borderRadius(24)
.margin({ bottom: 12 })
.enabled(this.remoteObject !== null)
.onClick(() => {
this.readDataFromService();
})
Button('✂️ 断开连接')
.width('80%')
.height(48)
.fontSize(16)
.backgroundColor('#F44336')
.fontColor(Color.White)
.borderRadius(24)
.margin({ bottom: 20 })
.enabled(this.remoteObject !== null)
.onClick(() => {
this.disconnectService();
})
// 读取结果展示
if (this.readData) {
Column() {
Text('读取结果:')
.fontSize(14)
.fontColor('#666666')
.margin({ bottom: 8 })
Text(this.readData)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.padding(12)
.backgroundColor('#E8F5E9')
.borderRadius(8)
.width('100%')
}
.width('80%')
.margin({ top: 8 })
}
// 数据键列表
if (this.dataKeys.length > 0) {
Column() {
Text('可用数据键:')
.fontSize(14)
.fontColor('#666666')
.margin({ bottom: 8 })
ForEach(this.dataKeys, (key: string) => {
Text(`• ${key}`)
.fontSize(14)
.margin({ bottom: 4 })
})
}
.width('80%')
.margin({ top: 16 })
}
}
.width('100%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
/**
* 连接到受保护的数据服务
*/
private connectToService(): void {
const context = this.getContext();
this.connectionStatus = '连接中...';
// 构建连接目标Want
const want: Want = {
bundleName: 'com.example.serviceprovider',
abilityName: 'DataServiceExtension'
};
// 连接回调
const connectOptions: AbilityConstant.ConnectOptions = {
// 连接成功回调
onConnect: (elementName, remote) => {
console.info('[Consumer] 连接成功');
this.remoteObject = remote;
this.connectionStatus = '已连接 ✅';
},
// 连接断开回调
onDisconnect: (elementName) => {
console.info('[Consumer] 连接断开');
this.remoteObject = null;
this.connectionStatus = '已断开';
},
// 连接失败回调
onFailed: (elementName) => {
console.error('[Consumer] 连接失败 - 可能没有权限');
this.remoteObject = null;
this.connectionStatus = '连接失败 ❌(检查权限)';
}
};
// 发起连接
try {
this.connectionId = context.connectServiceExtensionAbility(want, connectOptions);
console.info(`[Consumer] 连接请求已发送, ID: ${this.connectionId}`);
} catch (error) {
const err = error as BusinessError;
console.error(`[Consumer] 连接异常: ${err.code} - ${err.message}`);
this.connectionStatus = `连接异常 ❌ (${err.code})`;
}
}
/**
* 获取所有数据键
*/
private getAllKeys(): void {
if (!this.remoteObject) return;
const data = rpc.MessageSequence.create();
const reply = rpc.MessageSequence.create();
const option = new rpc.MessageOption();
try {
// 请求码3 = 获取所有键
this.remoteObject.sendRequest(3, data, reply, option);
const count = reply.readInt();
const keys: string[] = [];
for (let i = 0; i < count; i++) {
keys.push(reply.readString());
}
this.dataKeys = keys;
console.info(`[Consumer] 获取到 ${count} 个数据键`);
} catch (error) {
console.error('[Consumer] 获取数据键失败: ' + JSON.stringify(error));
}
}
/**
* 从服务读取数据
*/
private readDataFromService(): void {
if (!this.remoteObject || this.dataKeys.length === 0) return;
const key = this.dataKeys[0]; // 读取第一个键
const data = rpc.MessageSequence.create();
data.writeString(key);
const reply = rpc.MessageSequence.create();
const option = new rpc.MessageOption();
try {
// 请求码1 = 读取数据
this.remoteObject.sendRequest(1, data, reply, option);
const value = reply.readString();
this.readData = `${key} = ${value}`;
console.info(`[Consumer] 读取数据: ${key} = ${value}`);
} catch (error) {
console.error('[Consumer] 读取数据失败: ' + JSON.stringify(error));
}
}
/**
* 断开服务连接
*/
private disconnectService(): void {
if (this.connectionId === -1) return;
const context = this.getContext();
try {
context.disconnectServiceExtensionAbility(this.connectionId);
this.connectionId = -1;
this.remoteObject = null;
this.connectionStatus = '已断开';
this.readData = '';
this.dataKeys = [];
} catch (error) {
console.error('[Consumer] 断开连接失败: ' + JSON.stringify(error));
}
}
}
示例3:运行时权限校验工具
在服务端(应用 A),除了在 module.json5 中通过 permissions 字段做静态校验外,还可以在代码中进行运行时权限校验,实现更细粒度的访问控制:
// utils/CustomPermissionChecker.ets - 自定义权限运行时校验工具
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 校验结果
interface PermissionCheckResult {
isGranted: boolean; // 是否已授权
callerBundleName: string; // 调用者包名
permissionName: string; // 权限名称
errorMessage?: string; // 错误信息
}
export class CustomPermissionChecker {
private atManager: abilityAccessCtrl.AtManager;
constructor() {
this.atManager = abilityAccessCtrl.createAtManager();
}
/**
* 校验调用者是否拥有指定自定义权限
* @param context 上下文
* @param callerTokenId 调用者的Token ID
* @param permission 要校验的自定义权限名称
* @returns 校验结果
*/
async checkCustomPermission(
context: common.UIAbilityContext,
callerTokenId: number,
permission: string
): Promise<PermissionCheckResult> {
try {
// 使用 checkAccessToken 校验指定 Token ID 的权限
const grantStatus = await this.atManager.checkAccessToken(
callerTokenId,
permission as Permissions
);
const isGranted = grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
// 尝试获取调用者的包名
let callerBundleName = 'unknown';
try {
const bundleInfo = await bundleManager.getBundleInfoByTokenId(callerTokenId,
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT);
callerBundleName = bundleInfo.name;
} catch (e) {
console.warn('[PermChecker] 无法获取调用者包名');
}
return {
isGranted,
callerBundleName,
permissionName: permission,
errorMessage: isGranted ? undefined : `调用者 ${callerBundleName} 缺少权限 ${permission}`
};
} catch (error) {
const err = error as BusinessError;
return {
isGranted: false,
callerBundleName: 'unknown',
permissionName: permission,
errorMessage: `权限校验异常: ${err.code} - ${err.message}`
};
}
}
/**
* 批量校验多个自定义权限
* @param context 上下文
* @param callerTokenId 调用者的Token ID
* @param permissions 要校验的权限列表
* @returns 所有权限的校验结果
*/
async checkMultiplePermissions(
context: common.UIAbilityContext,
callerTokenId: number,
permissions: string[]
): Promise<PermissionCheckResult[]> {
const results: PermissionCheckResult[] = [];
for (const permission of permissions) {
const result = await this.checkCustomPermission(context, callerTokenId, permission);
results.push(result);
}
return results;
}
/**
* 校验调用者是否满足任一权限(OR逻辑)
* @param context 上下文
* @param callerTokenId 调用者的Token ID
* @param permissions 权限列表(满足任一即可)
* @returns 是否满足
*/
async checkAnyPermission(
context: common.UIAbilityContext,
callerTokenId: number,
permissions: string[]
): Promise<boolean> {
for (const permission of permissions) {
const result = await this.checkCustomPermission(context, callerTokenId, permission);
if (result.isGranted) {
return true;
}
}
return false;
}
/**
* 校验调用者是否满足所有权限(AND逻辑)
* @param context 上下文
* @param callerTokenId 调用者的Token ID
* @param permissions 权限列表(必须全部满足)
* @returns 是否全部满足
*/
async checkAllPermissions(
context: common.UIAbilityContext,
callerTokenId: number,
permissions: string[]
): Promise<boolean> {
for (const permission of permissions) {
const result = await this.checkCustomPermission(context, callerTokenId, permission);
if (!result.isGranted) {
return false;
}
}
return true;
}
/**
* 记录权限校验日志(用于审计)
* @param result 校验结果
* @param action 被保护的操作
*/
logPermissionCheck(result: PermissionCheckResult, action: string): void {
const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
caller: result.callerBundleName,
permission: result.permissionName,
granted: result.isGranted,
action,
error: result.errorMessage
};
if (result.isGranted) {
console.info(`[PermAudit] ✅ ${JSON.stringify(logEntry)}`);
} else {
console.warn(`[PermAudit] ❌ ${JSON.stringify(logEntry)}`);
}
}
}
在 ExtensionAbility 中使用运行时校验:
// extension/DataServiceExtension.ets - 增加运行时权限校验
import { ServiceExtensionAbility, Want } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { CustomPermissionChecker } from '../utils/CustomPermissionChecker';
export default class DataServiceExtension extends ServiceExtensionAbility {
private permChecker: CustomPermissionChecker = new CustomPermissionChecker();
onCreate(want: Want): void {
console.info('[DataService] 服务创建');
}
onConnect(want: Want): rpc.RemoteObject {
console.info('[DataService] 客户端连接请求');
// 获取调用者的Token ID
const callerTokenId = want.parameters?.['ohos.aafwk.param.callerTokenId'] as number;
if (callerTokenId) {
// 运行时权限校验(双重保险)
this.permChecker.checkCustomPermission(
this.context,
callerTokenId,
'com.example.serviceprovider.DATA_READ'
).then(result => {
this.permChecker.logPermissionCheck(result, 'connect_data_service');
if (!result.isGranted) {
console.error(`[DataService] 调用者 ${result.callerBundleName} 权限不足!`);
}
});
}
return new DataServiceRemoteObject('DataServiceStub');
}
onDestroy(): void {
console.info('[DataService] 服务销毁');
}
}
四、踩坑与注意事项
坑1:自定义权限名称冲突
现象:两个不同的应用定义了相同名称的自定义权限(如都叫 MY_PERMISSION),后安装的应用会注册失败。
原因:权限名称在系统中是全局唯一的,先到先得。
解决方案:严格使用包名作为前缀,确保命名空间隔离。
// ❌ 错误:没有包名前缀,容易冲突
"definePermissions": [
{ "name": "DATA_READ" }
]
// ✅ 正确:使用包名前缀
"definePermissions": [
{ "name": "com.example.myapp.DATA_READ" }
]
坑2:availableLevel 设置过高导致三方应用无法使用
现象:你定义了一个自定义权限,availableLevel 设为了 system_core,结果普通三方应用根本无法申请这个权限,你的服务形同虚设。
原因:三方应用的 APL 通常是 normal,只能申请 availableLevel 为 normal 的自定义权限。
解决方案:根据实际安全需求选择合适的级别。大多数场景下,normal 级别就够了。
坑3:ExtensionAbility 的 permissions 字段与运行时校验不一致
现象:你在 module.json5 的 permissions 字段中要求 DATA_READ 权限,但运行时校验代码中检查的是 DATA_WRITE 权限。结果有些调用者通过了静态校验却被运行时校验拒绝。
解决方案:保持静态校验和运行时校验的一致性。建议运行时校验作为静态校验的补充,而不是替代。
坑4:自定义权限的 grantMode 选择不当
现象:你把自定义权限的 grantMode 设为了 user_grant,但调用方安装时没有弹窗授权,运行时也无法请求。
原因:自定义权限的 user_grant 模式需要调用方在运行时主动请求。但如果调用方没有实现请求逻辑,权限就永远拿不到。
建议:对于自定义权限,优先使用 system_grant 模式。只有当权限涉及用户隐私且需要用户知情同意时,才使用 user_grant。
坑5:忽略权限校验的时序问题
现象:在 onConnect 回调中进行异步权限校验,但还没等校验结果返回,就已经返回了 RemoteObject,调用者已经开始发送请求。
原因:onConnect 必须同步返回 RemoteObject,异步校验无法阻止连接建立。
解决方案:在 RemoteObject 的 onRemoteRequest 方法中进行同步权限校验,或者在校验完成前拒绝处理请求。
class DataServiceRemoteObject extends rpc.RemoteObject {
private isVerified: boolean = false;
onRemoteRequest(code: number, data: rpc.MessageSequence, reply: rpc.MessageSequence, option: rpc.MessageOption): boolean {
// 在处理请求前校验权限
if (!this.isVerified) {
console.error('[DataService] 权限未验证,拒绝请求');
reply.writeInt(-1); // 返回错误码
return true;
}
// 正常处理请求
// ...
return true;
}
// 设置验证状态(由外部异步校验完成后调用)
setVerified(verified: boolean): void {
this.isVerified = verified;
}
}
五、HarmonyOS 6 适配
5.1 自定义权限增强
| 变化项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 权限定义 | definePermissions 数组 | 新增 provisionEnable 字段控制配置文件权限 |
| 运行时校验 | checkAccessToken | 新增 verifyPermission 方法,支持异步批量校验 |
| 权限审计 | 无 | 新增权限使用记录追踪自定义权限 |
| 跨设备权限 | 不支持 | 新增分布式权限同步机制 |
5.2 新增的 definePermissions 字段
// HarmonyOS 6 增强的自定义权限定义
"definePermissions": [
{
"name": "com.example.myapp.DATA_READ",
"grantMode": "system_grant",
"availableLevel": "normal",
"label": "$string:data_read_label",
"description": "$string:data_read_desc",
// HarmonyOS 6 新增字段
"provisionEnable": true, // 是否允许通过配置文件授权
"distributedSceneEnable": false // 是否允许在分布式场景下同步
}
]
5.3 迁移建议
- 为自定义权限添加
provisionEnable字段,明确是否允许配置文件授权 - 使用新增的
verifyPermission替代手动循环校验多个权限 - 如果应用涉及分布式场景,评估是否需要开启
distributedSceneEnable - 为所有自定义权限添加审计日志,记录权限的使用情况
六、总结
自定义权限知识图谱
├── 权限声明
│ ├── definePermissions 数组(定义方)
│ ├── requestPermissions 数组(申请方)
│ └── 命名规范:包名.类别.权限名
├── 关键字段
│ ├── name → 权限名称(全局唯一)
│ ├── grantMode → system_grant / user_grant
│ ├── availableLevel → normal / system_basic / system_core
│ ├── label → 简短标签
│ └── description → 详细描述
├── 应用场景
│ ├── 保护 ExtensionAbility
│ ├── 跨应用访问控制
│ ├── 组件级权限校验
│ └── 数据接口保护
├── 校验方式
│ ├── 静态校验 → permissions 字段
│ ├── 运行时校验 → checkAccessToken
│ ├── AND 逻辑 → 全部满足
│ └── OR 逻辑 → 任一满足
└── 安全考量
├── 命名冲突 → 使用包名前缀
├── 级别选择 → 满足需求即可
├── grantMode → 优先 system_grant
├── 时序问题 → 注意异步校验
└── 审计日志 → 记录权限使用
核心记忆口诀:
- 包名前缀,避免冲突——自定义权限名称必须以包名开头
- 级别够用,不要过高——availableLevel 选最低满足需求的
- 静态为主,运行时补——permissions 字段做主力,运行时校验做补充
- system 优先,user 慎用——自定义权限优先用 system_grant
- 注意时序,异步有坑——onConnect 必须同步返回,异步校验要延后到请求处理
- 记录日志,方便审计——权限校验结果要记录,出问题可追溯
自定义权限是鸿蒙安全体系中"应用级"的防线。它让你可以像设计门禁系统一样,精确控制谁能访问你的应用资源。但权力越大,责任越大——每一个自定义权限的声明和校验,都要经得起安全审计的考验。
- 点赞
- 收藏
- 关注作者
评论(0)