HarmonyOS APP声明、校验与跨应用权限安全实践

举报
Jack20 发表于 2026/06/20 13:39:31 2026/06/20
【摘要】 HarmonyOS APP声明、校验与跨应用权限安全实践📌 核心要点:自定义权限让应用可以定义自己的权限体系,实现跨应用访问控制、ExtensionAbility 保护、组件级权限校验等高级安全能力。但自定义权限涉及等级配置、跨应用校验、安全考量等多个维度,稍有不慎就会留下安全漏洞。 一、背景与动机想象你是一家连锁酒店的老板。酒店有公共区域(大堂、走廊),任何住客都能自由进出。但有些区域...

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{应用BAPL >= 权限的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 的受保护组件时:

  1. 应用 AdefinePermissions 中定义权限,在组件的 permissions 字段中引用该权限
  2. 应用 BrequestPermissions 中申请该权限
  3. 系统 在 B 安装时或运行时授予该权限
  4. 应用 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 迁移建议

  1. 为自定义权限添加 provisionEnable 字段,明确是否允许配置文件授权
  2. 使用新增的 verifyPermission 替代手动循环校验多个权限
  3. 如果应用涉及分布式场景,评估是否需要开启 distributedSceneEnable
  4. 为所有自定义权限添加审计日志,记录权限的使用情况

六、总结

自定义权限知识图谱
├── 权限声明
│   ├── definePermissions 数组(定义方)
│   ├── requestPermissions 数组(申请方)
│   └── 命名规范:包名.类别.权限名
├── 关键字段
│   ├── name → 权限名称(全局唯一)
│   ├── grantMode → system_grant / user_grant
│   ├── availableLevel → normal / system_basic / system_core
│   ├── label → 简短标签
│   └── description → 详细描述
├── 应用场景
│   ├── 保护 ExtensionAbility
│   ├── 跨应用访问控制
│   ├── 组件级权限校验
│   └── 数据接口保护
├── 校验方式
│   ├── 静态校验 → permissions 字段
│   ├── 运行时校验 → checkAccessToken
│   ├── AND 逻辑 → 全部满足
│   └── OR 逻辑 → 任一满足
└── 安全考量
    ├── 命名冲突 → 使用包名前缀
    ├── 级别选择 → 满足需求即可
    ├── grantMode → 优先 system_grant
    ├── 时序问题 → 注意异步校验
    └── 审计日志 → 记录权限使用

核心记忆口诀

  1. 包名前缀,避免冲突——自定义权限名称必须以包名开头
  2. 级别够用,不要过高——availableLevel 选最低满足需求的
  3. 静态为主,运行时补——permissions 字段做主力,运行时校验做补充
  4. system 优先,user 慎用——自定义权限优先用 system_grant
  5. 注意时序,异步有坑——onConnect 必须同步返回,异步校验要延后到请求处理
  6. 记录日志,方便审计——权限校验结果要记录,出问题可追溯

自定义权限是鸿蒙安全体系中"应用级"的防线。它让你可以像设计门禁系统一样,精确控制谁能访问你的应用资源。但权力越大,责任越大——每一个自定义权限的声明和校验,都要经得起安全审计的考验。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。