CHAPTER 09 · 10

原生能力与安全

深入 HarmonyOS NEXT 权限管理体系、系统能力 SysCap、加密与安全存储,以及服务卡片(Form Widget)开发。

HarmonyOS NEXT 权限管理体系

HarmonyOS NEXT 的权限体系是业界最细粒度之一,将权限按风险程度划分为多个等级,确保用户的隐私和安全得到精确保护。

Normal 权限
普通权限,风险极低。在 module.json5 中声明后即自动授权,用户感知不到。例如:INTERNET(网络访问)、KEEP_BACKGROUND_RUNNING(后台运行)等。
User_grant 权限
用户授权权限,涉及用户隐私敏感数据。必须在运行时通过系统弹框向用户申请,用户可以拒绝。例如:CAMERA、MICROPHONE、READ_CONTACTS、LOCATION 等。
System_grant 权限
系统授权权限,只有预置应用或具有特定签名的应用才能申请。普通开发者无法获取这类权限。例如:MANAGE_WIFI、INSTALL_BUNDLE 等系统级能力。
权限组
相关权限被分组管理,同一组内只要有一个权限被授予,同组其他权限申请时无需再次弹框。例如:CAMERA 和 MICROPHONE 同属媒体权限组。

完整的权限申请流程

import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'
import { common } from '@kit.AbilityKit'

/**
 * 完整权限申请工具类
 * 处理:已授权 / 未授权 / 已拒绝 / 永久拒绝 四种情况
 */
class PermissionHelper {

  static async requestPermissions(
    context: common.UIAbilityContext,
    permissions: Permissions[]
  ): Promise<'granted' | 'denied' | 'permanently_denied'> {

    const atManager = abilityAccessCtrl.createAtManager()

    // 1. 先检查当前授权状态
    const tokenId = await this.getTokenId(context)
    const allGranted = await Promise.all(permissions.map(async (perm) => {
      const status = await atManager.checkAccessToken(tokenId, perm)
      return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    }))

    if (allGranted.every(v => v)) {
      return 'granted'  // 全部已授权
    }

    // 2. 请求授权
    const result = await atManager.requestPermissionsFromUser(context, permissions)

    // 3. 检查结果
    const allGrantedNow = result.authResults.every(
      r => r === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    )
    if (allGrantedNow) return 'granted'

    // 4. 检查是否被永久拒绝(不再弹框)
    const permanentlyDenied = result.dialogShownResults.some(shown => !shown)
    return permanentlyDenied ? 'permanently_denied' : 'denied'
  }

  // 永久拒绝后,引导用户去设置页手动开启
  static async openPermissionSettings(context: common.UIAbilityContext) {
    const want: Want = {
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri: 'application_info_entry',
      parameters: {
        pushParams: context.applicationInfo.name
      }
    }
    await context.startAbility(want)
  }
}

// 在 UI 组件中使用
@Entry
@Component
struct CameraEntrance {
  build() {
    Button('打开相机')
      .onClick(async () => {
        const result = await PermissionHelper.requestPermissions(
          getContext(this) as common.UIAbilityContext,
          ['ohos.permission.CAMERA']
        )

        switch (result) {
          case 'granted':
            // 进入相机页面
            break
          case 'denied':
            promptAction.showToast({ message: '需要相机权限才能使用此功能' })
            break
          case 'permanently_denied':
            // 弹框引导用户去设置页
            await PermissionHelper.openPermissionSettings(
              getContext(this) as common.UIAbilityContext
            )
            break
        }
      })
  }
}

系统能力 SysCap

SystemCapability(SysCap)是 HarmonyOS NEXT 描述设备能力的体系。由于鸿蒙生态覆盖手机、平板、手表、电视、车机等众多设备,不同设备支持的能力不同(手表没有相机,电视没有触摸屏)。SysCap 机制让开发者在代码层面声明"此功能需要某种系统能力",AGC 审核和设备过滤时据此判断兼容性:

// 检查设备是否支持特定系统能力
import deviceInfo from '@ohos.deviceInfo'

// 判断设备类型
function getDeviceType(): string {
  return deviceInfo.deviceType  // 'phone' | 'tablet' | 'tv' | 'wearable' | '2in1'
}

// 根据设备类型展示不同 UI
@Entry
@Component
struct AdaptiveLayout {
  @State deviceType: string = deviceInfo.deviceType

  build() {
    if (this.deviceType === 'tablet' || this.deviceType === '2in1') {
      // 平板/折叠屏:双栏布局
      Row() {
        SideBar().width(280)
        MainContent().layoutWeight(1)
      }
    } else {
      // 手机:单栏布局
      Column() {
        MainContent().width('100%')
      }
    }
  }
}

// 原子化服务(卡片、快捷方式)需要声明 SysCap
// module.json5 中:
// "syscap": "SystemCapability.Window.SessionManager"

加密与安全存储

HarmonyOS NEXT 提供了完整的加密工具包(@ohos.security.cryptoFramework),以及受硬件保护的安全存储(Huks,Hardware Universal Key Store):

import huks from '@ohos.security.huks'

// HUKS 是华为统一密钥库服务
// 密钥存储在 TEE(可信执行环境)中,不可导出
// 即使设备被 root,密钥也无法被提取

class SecureKeyManager {
  private static KEY_ALIAS = 'app_master_key'

  // 生成 AES-256-GCM 密钥(存储在 TEE 中)
  static async generateKey() {
    const keyAlias = SecureKeyManager.KEY_ALIAS
    const options: huks.HuksOptions = {
      properties: [
        { tag: huks.HuksTag.HUKS_TAG_ALGORITHM,    value: huks.HuksKeyAlg.HUKS_ALG_AES },
        { tag: huks.HuksTag.HUKS_TAG_KEY_SIZE,      value: huks.HuksKeySize.HUKS_AES_KEY_SIZE_256 },
        { tag: huks.HuksTag.HUKS_TAG_PURPOSE,       value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT | huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT },
        { tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE,    value: huks.HuksCipherMode.HUKS_MODE_GCM },
        { tag: huks.HuksTag.HUKS_TAG_PADDING,       value: huks.HuksKeyPadding.HUKS_PADDING_NONE },
      ]
    }
    await huks.generateKeyItem(keyAlias, options)
    console.log('主密钥已在 TEE 中生成')
  }

  // 加密数据
  static async encrypt(plainText: string): Promise<Uint8Array> {
    const textEncoder = new TextEncoder()
    const plainBuffer = textEncoder.encode(plainText)
    // ... 使用 HUKS session 执行加密
    return plainBuffer  // 简化示例
  }
}

// 使用 cryptoFramework 进行哈希和 HMAC
import cryptoFramework from '@ohos.security.cryptoFramework'

async function hashPassword(password: string): Promise<string> {
  const mdAlgo = cryptoFramework.createMd('SHA256')
  const textEncoder = new TextEncoder()
  const encoded = textEncoder.encode(password)

  await mdAlgo.update({ data: encoded })
  const digestData = await mdAlgo.digest()

  // 转为十六进制字符串
  return Array.from(digestData.data)
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

服务卡片(Form Widget)

服务卡片是 HarmonyOS NEXT 的核心差异化功能之一。用户可以将应用的信息摘要以"卡片"形式固定在桌面或服务中心,无需打开应用即可查看实时数据、执行快捷操作。

FormExtensionAbility
服务卡片的提供者,继承自 ExtensionAbility。负责响应卡片生命周期(创建、更新、销毁),向桌面提供卡片数据。
ArkUI 卡片 UI
卡片的界面同样用 ArkTS + ArkUI 编写,但有严格限制:不支持自定义组件、不能使用网络请求(只读卡片数据)、不能访问 AppStorage。UI 由主应用进程预渲染后发送给桌面展示。
// module.json5 — 声明服务卡片
{
  "extensionAbilities": [{
    "name": "WeatherFormAbility",
    "srcEntry": "./ets/formability/WeatherFormAbility.ets",
    "type": "form",
    "metadata": [{
      "name": "ohos.extension.form",
      "resource": "$profile:form_config"
    }]
  }]
}

// WeatherFormAbility.ets — 卡片提供者
import { FormExtensionAbility, formBindingData, formInfo, formProvider } from '@kit.FormKit'

export default class WeatherFormAbility extends FormExtensionAbility {

  // 卡片首次创建时调用,返回初始数据
  onAddForm(want: Want): formBindingData.FormBindingData {
    const data = {
      temperature: '24°C',
      weather: '晴天',
      city: '深圳',
      icon: '☀️'
    }
    return formBindingData.createFormBindingData(data)
  }

  // 卡片事件处理(用户点击卡片上的按钮)
  onFormEvent(formId: string, message: string) {
    console.log(`卡片 ${formId} 收到事件: ${message}`)
    if (message === 'refresh') {
      this.updateWeather(formId)
    }
  }

  async updateWeather(formId: string) {
    // 从服务端获取最新天气(在服务层,而非卡片 UI 层)
    const weather = await fetchWeatherData()
    const newData = {
      temperature: weather.temp,
      weather: weather.description,
      city: weather.city,
      icon: this.getWeatherIcon(weather.code)
    }
    // 推送新数据给桌面卡片
    await formProvider.updateForm(formId, formBindingData.createFormBindingData(newData))
  }

  getWeatherIcon(code: number): string {
    const icons: Record<number, string> = {
      100: '☀️', 101: '⛅', 300: '🌧️', 400: '❄️'
    }
    return icons[code] ?? '🌤️'
  }
}

// widget/pages/WeatherCard.ets — 卡片 UI(ArkUI 受限版)
let storageWidgetData = new LocalStorage()
@Entry(storageWidgetData)
@Component
struct WeatherCard {
  @LocalStorageProp('temperature') temperature: string = '--°C'
  @LocalStorageProp('weather') weather: string = '--'
  @LocalStorageProp('city') city: string = '--'
  @LocalStorageProp('icon') icon: string = '🌤️'

  build() {
    Column({ space: 4 }) {
      Text(this.city).fontSize(14).fontColor('#adbac7')
      Text(this.icon).fontSize(36)
      Text(this.temperature)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#e6edf3')
      Text(this.weather).fontSize(12).fontColor('#768390')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#161b22')
    .borderRadius(16)
  }
}
服务卡片最佳实践