HarmonyOS开发:HarmonyOS 6安全——零信任安全模型
HarmonyOS开发:HarmonyOS 6安全——零信任安全模型
📌 核心要点:HarmonyOS 6引入零信任安全架构,从"一次认证、全程信任"升级到"持续认证、动态授权",数据全链路加密,开发者必须适配新的权限申请流程和动态授权机制。
背景与动机
你的App在HarmonyOS 5上申请了位置权限,用户点了"允许"——然后呢?然后你的App就能一直获取位置,用户走到哪你都知道,直到用户手动去设置里关掉。
这合理吗?显然不合理。
传统安全模型是"城堡模型":进了城堡就信任你,出了城堡就不信任。问题是,进了城堡之后你干什么,没人管。应用拿到了权限,是正常使用还是过度收集?用户不知道,系统也不管。
HarmonyOS 6的零信任安全模型要解决这个问题。核心思想:永远不默认信任,每次访问都验证,权限随时可收回。
对开发者来说,这意味着你不能再"一次申请、永久使用"了。权限可能随时被撤销,你的代码必须处理"有权限"和"没权限"两种情况——而且要优雅地处理,不能崩。
这篇文章把零信任安全模型的核心机制、适配方法、踩坑经验全讲清楚。
核心原理
零信任安全模型的三个核心原则:持续认证、动态授权、最小权限。
flowchart TD
A[零信任安全模型] --> B[持续认证]
A --> C[动态授权]
A --> D[数据全链路加密]
B --> B1[设备认证]
B --> B2[用户认证]
B --> B3[应用认证]
B --> B4[行为风险评估]
C --> C1[权限按需申请]
C --> C2[权限限时生效]
C --> C3[权限可随时撤销]
C --> C4[敏感操作二次确认]
D --> D1[传输加密TLS 1.3]
D --> D2[存储加密AES-256]
D --> D3[内存加密]
D --> D4[安全飞地TEE]
classDef root fill:#C62828,color:#fff,stroke:#B71C1C
classDef auth fill:#1565C0,color:#fff,stroke:#0D47A1
classDef authz fill:#2E7D32,color:#fff,stroke:#1B5E20
classDef encrypt fill:#6A1B9A,color:#fff,stroke:#4A148C
class A,root
class B,B1,B2,B3,B4,auth
class C,C1,C2,C3,C4,authz
class D,D1,D2,D3,D4,encrypt
V5 vs V6:安全模型对比
| 维度 | V5(城堡模型) | V6(零信任模型) |
|---|---|---|
| 认证方式 | 一次认证 | 持续认证 |
| 权限生效 | 安装时声明,永久生效 | 使用时申请,限时生效 |
| 权限撤销 | 用户手动去设置关 | 系统自动/用户随时撤销 |
| 敏感操作 | 一次授权即可 | 每次操作需确认 |
| 数据加密 | 传输加密 | 全链路加密(传输+存储+内存) |
| 风险评估 | 无 | 实时行为风险评估 |
动态授权机制
V6的权限分为三个敏感级别:
| 级别 | 示例权限 | 授权方式 | 有效期 |
|---|---|---|---|
| 普通权限 | 网络访问、振动 | 安装时自动授予 | 永久 |
| 敏感权限 | 位置、相机、麦克风 | 使用时弹窗申请 | 可配置(本次/1天/永久) |
| 高敏感权限 | 后台位置、通话记录 | 使用时申请+系统二次确认 | 仅本次 |
高敏感权限是V6新增的分类。即使应用已经获得了位置权限,如果要在后台持续获取位置,还需要额外申请后台位置权限——而且这个权限只能"本次有效"。
行为风险评估
V6引入了实时行为风险评估引擎。系统会监控应用的行为模式,发现异常时自动降权:
- 应用在后台频繁读取位置 → 自动撤销后台位置权限
- 应用短时间内大量读取通讯录 → 触发异常告警
- 应用在非活跃时段大量上传数据 → 限制网络访问
代码实战
基础用法:动态权限申请
// 动态权限申请 - V6新方式
import { abilityAccessCtrl } from '@ohos.abilityAccessCtrl'
import { bundleManager } from '@ohos.bundleManager'
// 权限配置
interface PermissionConfig {
name: string
reason: string // 申请理由(V6必填,会展示给用户)
sensitiveLevel: 'normal' | 'sensitive' | 'highly_sensitive'
validDuration?: 'once' | 'one_day' | 'forever' // 有效期
}
// 应用需要的权限列表
const APP_PERMISSIONS: PermissionConfig[] = [
{
name: 'ohos.permission.APPROXIMATELY_LOCATION',
reason: '用于展示您附近的商店信息',
sensitiveLevel: 'sensitive',
validDuration: 'one_day'
},
{
name: 'ohos.permission.LOCATION',
reason: '用于精确导航到目的地',
sensitiveLevel: 'highly_sensitive',
validDuration: 'once'
},
{
name: 'ohos.permission.CAMERA',
reason: '用于扫描二维码和拍照',
sensitiveLevel: 'sensitive',
validDuration: 'one_day'
}
]
@Entry
@Component
struct PermissionDemoPage {
@State permissionStatus: Map<string, string> = new Map()
@State locationText: string = '未获取位置'
private atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager()
aboutToAppear() {
this.checkAllPermissions()
}
// 检查所有权限状态
async checkAllPermissions() {
for (const perm of APP_PERMISSIONS) {
const status = await this.checkPermission(perm.name)
this.permissionStatus.set(perm.name, status)
}
this.permissionStatus = new Map(this.permissionStatus)
}
// 检查单个权限
async checkPermission(permission: string): Promise<string> {
try {
const tokenID = await this.getTokenID()
const result = await this.atManager.checkAccessToken(tokenID, permission)
return result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
? '已授权' : '未授权'
} catch (err) {
return '检查失败'
}
}
// 请求权限(V6新方式:带理由+有效期)
async requestPermission(config: PermissionConfig): Promise<boolean> {
try {
const context = getContext(this) as common.UIAbilityContext
const result = await this.atManager.requestPermissionsFromUser(context, {
permissions: [config.name],
// 🔑 V6新增:申请理由(必须提供,否则直接拒绝)
reason: config.reason,
// 🔑 V6新增:有效期选择
validDuration: config.validDuration ?? 'once'
})
if (result.authResults[0] === 0) {
this.permissionStatus.set(config.name, '已授权')
this.permissionStatus = new Map(this.permissionStatus)
return true
}
return false
} catch (err) {
console.error(`权限申请失败: ${JSON.stringify(err)}`)
return false
}
}
// 获取TokenID
private async getTokenID(): Promise<number> {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT
)
return bundleInfo.appInfo.accessTokenId
}
// 获取位置(需要先检查权限)
async getLocation() {
// 🔑 关键:每次使用前都要检查权限
const hasPermission = await this.checkPermission('ohos.permission.LOCATION')
if (hasPermission !== '已授权') {
this.locationText = '没有位置权限,请先授权'
return
}
try {
// 获取位置...
this.locationText = '当前位置: 北京市海淀区 (39.9°N, 116.3°E)'
} catch (err) {
this.locationText = `获取位置失败: ${err}`
}
}
build() {
Column({ space: 16 }) {
Text('零信任权限管理')
.fontSize(20)
.fontWeight(FontWeight.Bold)
// 权限列表
ForEach(APP_PERMISSIONS, (perm: PermissionConfig) => {
Row() {
Column({ space: 4 }) {
Text(perm.name.replace('ohos.permission.', ''))
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(`级别: ${perm.sensitiveLevel} | 有效期: ${perm.validDuration}`)
.fontSize(12)
.fontColor('#666666')
Text(`理由: ${perm.reason}`)
.fontSize(12)
.fontColor('#999999')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Text(this.permissionStatus.get(perm.name) ?? '检查中')
.fontSize(13)
.fontColor(
this.permissionStatus.get(perm.name) === '已授权'
? '#4CAF50' : '#F44336'
)
}
.width('100%')
.padding(12)
.backgroundColor(Color.White)
.borderRadius(8)
Button(
this.permissionStatus.get(perm.name) === '已授权' ? '已授权' : '申请权限'
)
.width('100%')
.height(40)
.enabled(this.permissionStatus.get(perm.name) !== '已授权')
.onClick(() => this.requestPermission(perm))
}, (perm: PermissionConfig) => perm.name)
// 位置获取
Button('获取当前位置')
.width('100%')
.height(44)
.onClick(() => this.getLocation())
Text(this.locationText)
.fontSize(14)
.padding(12)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.width('100%')
}
.width('100%')
.height('100%')
.padding(16)
}
}
进阶用法:权限状态监听与降级处理
零信任模型下,权限随时可能被撤销。你的代码必须监听权限变化,优雅地处理权限丢失。
// 权限状态监听与降级处理
import { abilityAccessCtrl } from '@ohos.abilityAccessCtrl'
// 权限降级策略
interface FallbackStrategy {
permission: string
fallbackMessage: string
fallbackAction: () => void
}
@Entry
@Component
struct PermissionMonitorPage {
@State cameraAvailable: boolean = false
@State locationAvailable: boolean = false
@State microphoneAvailable: boolean = false
@State statusMessage: string = '初始化中...'
@State capturedPhoto: string = ''
private atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager()
private fallbackStrategies: FallbackStrategy[] = []
aboutToAppear() {
this.setupPermissionMonitor()
this.setupFallbackStrategies()
}
// 设置权限状态监听
setupPermissionMonitor() {
// 🔑 V6新增:监听权限变更
// 权限被撤销时自动收到通知
this.atManager.on('permissionChange', (data: abilityAccessCtrl.PermissionChangeInfo) => {
console.info(`权限变更: ${JSON.stringify(data)}`)
// 根据变更类型更新UI状态
for (const change of data.changes) {
if (change.grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {
// 权限被撤销,执行降级策略
this.handlePermissionRevoked(change.permissionName)
} else {
// 权限被授予,更新状态
this.handlePermissionGranted(change.permissionName)
}
}
})
}
// 设置降级策略
setupFallbackStrategies() {
this.fallbackStrategies = [
{
permission: 'ohos.permission.CAMERA',
fallbackMessage: '相机权限已关闭,无法拍照。你可以从相册选择图片。',
fallbackAction: () => { this.cameraAvailable = false }
},
{
permission: 'ohos.permission.LOCATION',
fallbackMessage: '位置权限已关闭,将使用上次已知位置。',
fallbackAction: () => { this.locationAvailable = false }
},
{
permission: 'ohos.permission.MICROPHONE',
fallbackMessage: '麦克风权限已关闭,无法录音。',
fallbackAction: () => { this.microphoneAvailable = false }
}
]
}
// 处理权限被撤销
private handlePermissionRevoked(permission: string) {
const strategy = this.fallbackStrategies.find(
s => s.permission === permission
)
if (strategy) {
this.statusMessage = strategy.fallbackMessage
strategy.fallbackAction()
}
}
// 处理权限被授予
private handlePermissionGranted(permission: string) {
if (permission === 'ohos.permission.CAMERA') this.cameraAvailable = true
if (permission === 'ohos.permission.LOCATION') this.locationAvailable = true
if (permission === 'ohos.permission.MICROPHONE') this.microphoneAvailable = true
this.statusMessage = '权限已更新'
}
// 安全地使用相机
async safeCapturePhoto() {
// 🔑 关键:使用前再次确认权限
const hasPermission = await this.checkPermission('ohos.permission.CAMERA')
if (!hasPermission) {
this.statusMessage = '需要相机权限才能拍照,请授权后重试'
// 引导用户授权
await this.requestPermission({
name: 'ohos.permission.CAMERA',
reason: '用于拍照记录',
sensitiveLevel: 'sensitive',
validDuration: 'once'
})
return
}
try {
// 执行拍照...
this.capturedPhoto = 'photo_' + Date.now() + '.jpg'
this.statusMessage = '拍照成功'
} catch (err) {
// 权限可能在拍照过程中被撤销
this.statusMessage = '拍照失败,权限可能已变更'
this.cameraAvailable = false
}
}
private async checkPermission(permission: string): Promise<boolean> {
try {
const tokenID = 0 // 实际获取
const result = await this.atManager.checkAccessToken(tokenID, permission)
return result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
} catch {
return false
}
}
private async requestPermission(config: PermissionConfig): Promise<boolean> {
// 同上
return false
}
build() {
Column({ space: 16 }) {
Text('权限状态监控')
.fontSize(20)
.fontWeight(FontWeight.Bold)
// 权限状态指示器
Row({ space: 16 }) {
this.StatusIndicator('📷', '相机', this.cameraAvailable)
this.StatusIndicator('📍', '位置', this.locationAvailable)
this.StatusIndicator('🎙️', '麦克风', this.microphoneAvailable)
}
// 状态消息
Text(this.statusMessage)
.fontSize(14)
.padding(12)
.backgroundColor('#FFF3E0')
.borderRadius(8)
.width('100%')
.fontColor('#E65100')
// 功能按钮
Button('拍照')
.width('100%')
.height(44)
.enabled(this.cameraAvailable)
.onClick(() => this.safeCapturePhoto())
if (this.capturedPhoto) {
Text(`已拍照: ${this.capturedPhoto}`)
.fontSize(14)
.fontColor('#4CAF50')
}
}
.width('100%')
.height('100%')
.padding(16)
}
@Builder
StatusIndicator(icon: string, label: string, available: boolean) {
Column({ space: 4 }) {
Text(icon)
.fontSize(28)
Text(label)
.fontSize(12)
Circle({ width: 8, height: 8 })
.fill(available ? '#4CAF50' : '#F44336')
}
.padding(12)
.backgroundColor(Color.White)
.borderRadius(12)
}
}
完整示例:数据全链路加密
// 数据全链路加密 - 存储加密 + 传输加密
import { cipher } from '@ohos.security.cipher'
import { http } from '@ohos.net.http'
// 加密配置
interface CryptoConfig {
algorithm: 'AES-256-GCM' | 'AES-256-CBC'
keyDerivation: 'PBKDF2'
iterations: number
}
// 安全数据管理器
export class SecureDataManager {
private config: CryptoConfig = {
algorithm: 'AES-256-GCM',
keyDerivation: 'PBKDF2',
iterations: 100000
}
private encryptionKey: Uint8Array | null = null
// 初始化加密密钥(从安全存储获取或派生)
async initKey(userId: string, userPin: string) {
try {
// 从设备安全飞地(TEE)获取盐值
const salt = await this.getDeviceSalt(userId)
// 使用PBKDF2派生密钥
this.encryptionKey = await cipher.deriveKey({
algorithm: this.config.keyDerivation,
password: userPin,
salt: salt,
iterations: this.config.iterations,
keySize: 256 // AES-256
})
} catch (err) {
console.error(`密钥初始化失败: ${JSON.stringify(err)}`)
}
}
// 加密数据
async encrypt(plaintext: string): Promise<string> {
if (!this.encryptionKey) throw new Error('密钥未初始化')
const encrypted = await cipher.encrypt({
algorithm: this.config.algorithm,
key: this.encryptionKey,
data: new TextEncoder().encode(plaintext)
})
// 返回Base64编码的密文+IV+Tag
return this.encodeCipherResult(encrypted)
}
// 解密数据
async decrypt(ciphertext: string): Promise<string> {
if (!this.encryptionKey) throw new Error('密钥未初始化')
const cipherData = this.decodeCipherResult(ciphertext)
const decrypted = await cipher.decrypt({
algorithm: this.config.algorithm,
key: this.encryptionKey,
data: cipherData.data,
iv: cipherData.iv,
tag: cipherData.tag
})
return new TextDecoder().decode(decrypted)
}
// 安全存储(加密后存储到本地)
async secureStore(key: string, value: string) {
const encrypted = await this.encrypt(value)
// 存储到应用沙箱
const prefs = await this.getPreferences()
await prefs.put(key, encrypted)
await prefs.flush()
}
// 安全读取(从本地读取后解密)
async secureRead(key: string): Promise<string | null> {
const prefs = await this.getPreferences()
const encrypted = await prefs.get(key, '') as string
if (!encrypted) return null
return await this.decrypt(encrypted)
}
// 安全网络请求(TLS 1.3 + 数据加密)
async secureRequest(url: string, data: Record<string, Object>): Promise<string> {
// 先加密请求体
const encryptedBody = await this.encrypt(JSON.stringify(data))
const client = http.createHttpClient()
const response = await client.request(url, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'X-Encryption': 'AES-256-GCM' // 告知服务端数据已加密
},
extraData: JSON.stringify({ encrypted: encryptedBody })
})
client.destroy()
if (response.responseCode !== 200) {
throw new Error(`请求失败: ${response.responseCode}`)
}
// 解密响应
const responseData = JSON.parse(response.result as string)
if (responseData.encrypted) {
return await this.decrypt(responseData.encrypted)
}
return response.result as string
}
// 辅助方法
private async getDeviceSalt(userId: string): Promise<Uint8Array> {
// 从TEE获取设备唯一盐值
const salt = new TextEncoder().encode(`salt_${userId}_device`)
return salt
}
private encodeCipherResult(result: cipher.EncryptResult): string {
// 实际实现:Base64编码 IV + 密文 + Tag
return btoa(String.fromCharCode(...result.data, ...result.iv))
}
private decodeCipherResult(encoded: string): cipher.EncryptResult {
// 实际实现:Base64解码并分离 IV + 密文 + Tag
const decoded = Uint8Array.from(atob(encoded), c => c.charCodeAt(0))
return {
data: decoded.slice(0, -12),
iv: decoded.slice(-12),
tag: new Uint8Array(16)
}
}
private async getPreferences() {
// 获取应用偏好设置
return null as any // 简化实现
}
}
// 页面中使用
@Entry
@Component
struct SecureDataPage {
@State inputData: string = ''
@State encryptedData: string = ''
@State decryptedData: string = ''
@State statusText: string = ''
private secureManager: SecureDataManager = new SecureDataManager()
async aboutToAppear() {
// 初始化加密密钥
await this.secureManager.initKey('user_123', 'user_pin')
this.statusText = '加密模块就绪'
}
// 加密并存储
async encryptAndStore() {
if (!this.inputData.trim()) return
try {
this.encryptedData = await this.secureManager.encrypt(this.inputData)
await this.secureManager.secureStore('user_note', this.inputData)
this.statusText = '加密存储成功'
} catch (err) {
this.statusText = `加密失败: ${err}`
}
}
// 读取并解密
async readAndDecrypt() {
try {
this.decryptedData = await this.secureManager.secureRead('user_note') ?? '(无数据)'
this.statusText = '解密读取成功'
} catch (err) {
this.statusText = `解密失败: ${err}`
}
}
build() {
Column({ space: 16 }) {
Text('数据全链路加密')
.fontSize(20)
.fontWeight(FontWeight.Bold)
TextInput({ text: $$this.inputData, placeholder: '输入要加密的数据' })
.width('100%')
.height(44)
Button('加密并存储')
.width('100%')
.height(44)
.onClick(() => this.encryptAndStore())
if (this.encryptedData) {
Text('密文:')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.width('100%')
Scroll() {
Text(this.encryptedData)
.fontSize(12)
.fontFamily('monospace')
}
.height(80)
.width('100%')
.padding(8)
.backgroundColor('#F5F5F5')
.borderRadius(8)
}
Button('读取并解密')
.width('100%')
.height(44)
.onClick(() => this.readAndDecrypt())
if (this.decryptedData) {
Text(`明文: ${this.decryptedData}`)
.fontSize(14)
.padding(12)
.backgroundColor('#E8F5E9')
.borderRadius(8)
.width('100%')
}
Text(this.statusText)
.fontSize(14)
.fontColor('#1565C0')
}
.width('100%')
.height('100%')
.padding(16)
}
}
踩坑与注意事项
权限适配的坑
坑1:V5的静态权限声明在V6上不自动生效
V5里你在module.json5声明了ohos.permission.LOCATION,安装时自动授予。V6里,敏感权限声明只是"告诉系统你可能需要这个权限",不等于"你已经有这个权限"。必须运行时调用requestPermissionsFromUser。
坑2:权限申请理由不能为空
V6的requestPermissionsFromUser必须提供reason参数,而且理由必须跟实际用途匹配。写"用于提升用户体验"这种万能理由?系统可能直接拒绝。
坑3:高敏感权限只能"本次有效"
后台位置、通话记录这类高敏感权限,用户只能选"本次允许",没有"永久允许"选项。你的代码不能假设权限一直存在,每次使用前都要重新检查。
权限监听的坑
坑4:权限被撤销时,正在执行的操作不会中断
用户撤销了相机权限,但你正在拍照——拍照操作不会自动停止,但结果可能无法保存。你需要在操作完成后检查权限状态,如果权限已撤销,丢弃结果。
坑5:权限变更回调可能延迟
权限撤销通知不是实时的,可能有几百毫秒的延迟。你的代码不应该依赖"权限变更后立即收到通知"这个假设。
数据加密的坑
坑6:密钥管理是最难的部分
加密算法本身不难,难的是密钥怎么存、怎么传、怎么更新。密钥存在代码里?反编译就暴露了。存在本地文件里?root设备就能读。最佳方案是用设备的TEE(可信执行环境)来管理密钥。
坑7:加密数据不能直接搜索
加密后的数据是乱码,你没法对密文做全文搜索。如果需要搜索加密数据,要么用可搜索加密(复杂),要么在服务端解密后搜索(隐私风险),要么维护一个加密索引。
坑8:密钥轮换会导致旧数据无法解密
如果你更换了加密密钥,用旧密钥加密的数据就解不开了。密钥轮换时必须用旧密钥解密、新密钥重新加密——这个过程可能很长,需要后台异步处理。
HarmonyOS 6适配说明
零信任安全模型的适配是P0优先级——不适配,你的应用可能无法获取任何敏感权限。
| 适配项 | 说明 | 工作量 |
|---|---|---|
| 权限申请代码改造 | 静态声明→动态申请 | 2-3天 |
| 权限理由文案编写 | 每个敏感权限都需要理由 | 1天 |
| 权限状态监听 | 处理权限被撤销的情况 | 2-3天 |
| 降级策略实现 | 权限不可用时的替代方案 | 2-5天 |
| 数据加密接入 | 可选,但对敏感数据推荐 | 3-5天 |
module.json5变更
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.APPROXIMATELY_LOCATION",
"reason": "$string:location_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.LOCATION",
"reason": "$string:precise_location_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
V6要求每个敏感权限必须声明reason(引用字符串资源)和usedScene(使用场景和时机)。
总结
零信任安全模型是HarmonyOS 6对开发者影响最大的变化之一。从"一次授权永久使用"到"每次使用都要验证",这个转变要求你重新设计权限管理逻辑。
但往好处想,零信任模型让你的应用更安全,用户也更放心。用户知道你的应用不会在后台偷偷收集数据,信任度反而更高。
适配零信任模型的核心原则:永远不要假设权限存在,每次使用前检查,权限丢失时优雅降级。把这三句话刻在脑子里,你的适配工作就不会出大问题。
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 安全概念多,密钥管理复杂 |
| 使用频率 | ⭐⭐⭐⭐⭐ 每个涉及敏感权限的应用都必须适配 |
| 重要程度 | ⭐⭐⭐⭐⭐ 不适配就无法获取敏感权限 |
下一步:了解V6的渲染引擎怎么让UI更丝滑——看第598篇《HarmonyOS 6性能:渲染引擎升级》。
- 点赞
- 收藏
- 关注作者
评论(0)