HarmonyOS开发从隐私看板到数据删除,构建用户信任的隐私防线
HarmonyOS APP开发中的隐私设置全解析:从隐私看板到数据删除,构建用户信任的隐私防线
📌 核心要点:HarmonyOS 提供完整的隐私管理框架,涵盖隐私看板、权限使用记录、广告ID管理、数据删除请求和隐私合规开发,帮助开发者构建可信赖的应用
一、背景与动机
2024 年,某知名 App 因"偷偷调用摄像头"被曝光,下载量一夜之间暴跌 40%。这不是个例——用户对隐私的敏感度前所未有地高,一条"该应用正在使用你的位置"的系统提示,就可能让用户直接卸载。
隐私不是"锦上添花",而是"生死线"。在 HarmonyOS 生态中,隐私合规更是上架审核的硬性要求。如果你的应用没有正确处理权限申请、没有提供数据删除入口、没有尊重用户的隐私选择——对不起,审核都过不了。
HarmonyOS 提供了一套完整的隐私管理框架,从隐私看板(让用户看到谁在用什么权限)到数据删除请求(让用户可以"被遗忘"),覆盖了隐私保护的完整生命周期。
典型使用场景:
- 隐私看板:展示应用权限使用情况,增强透明度
- 权限使用记录:记录每次权限调用的详细日志
- 广告ID管理:尊重用户的广告追踪选择
- 数据删除请求:响应用户的"被遗忘权"
- 隐私合规开发:遵循最小权限原则和透明度原则
二、核心原理
2.1 隐私管理体系架构
flowchart TB
A[用户] --> B[隐私看板]
A --> C[权限管理]
A --> D[广告ID设置]
A --> E[数据删除请求]
B --> B1[权限使用概览]
B --> B2[敏感行为记录]
B --> B3[风险提醒]
C --> C1[权限申请]
C --> C2[权限授权]
C --> C3[权限撤销]
C --> C4[使用记录]
D --> D1[广告ID生成]
D --> D2[重置广告ID]
D --> D3[限制广告追踪]
E --> E1[请求数据删除]
E --> E2[应用处理删除]
E --> E3[确认删除完成]
F[开发者] --> G[隐私合规开发]
G --> G1[最小权限原则]
G --> G2[透明度原则]
G --> G3[数据最小化]
G --> G4[安全存储]
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 primary
class B,C,D,E info
class B1,B2,B3,C1,C2,C3,C4,D1,D2,D3,E1,E2,E3 purple
class F warning
class G error
class G1,G2,G3,G4 primary
2.2 权限使用记录机制
HarmonyOS 会自动记录应用使用敏感权限的行为,包括:
| 记录项 | 说明 |
|---|---|
| 权限名称 | 使用的具体权限(如 CAMERA、LOCATION) |
| 使用时间 | 精确到秒的时间戳 |
| 使用时长 | 持续使用的时间 |
| 访问频率 | 一段时间内的使用次数 |
| 前后台状态 | 使用时应用是否在前台 |
2.3 隐私合规开发流程
flowchart LR
A[需求分析] --> B{是否需要敏感权限?}
B -->|否| C[正常开发]
B -->|是| D[评估必要性]
D --> E{是否必须?}
E -->|否| F[寻找替代方案]
E -->|是| G[运行时申请权限]
G --> H[用户授权?]
H -->|是| I[使用权限+记录日志]
H -->|否| J[降级处理]
I --> K[用完即释放]
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 primary
class B,D,E,H info
class C,G,I,K primary
class F,J warning
核心理念:最小权限原则——能用不敏感的权限就别用敏感的,能不用权限就别申请。就像你不会把家里所有房间的钥匙都交给保洁阿姨,只给她需要打扫的房间钥匙就够了。
三、代码实战
3.1 隐私看板与权限使用记录:让权限使用透明化
隐私看板是 HarmonyOS 的系统级功能,应用开发者需要确保权限使用行为被正确记录和展示。
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 权限使用记录项
*/
interface PermissionUsageRecord {
permission: string; // 权限名称
permissionName: string; // 权限中文名
lastUsedTime: number; // 最后使用时间
useCount: number; // 使用次数
isForeground: boolean; // 是否前台使用
}
/**
* 隐私看板管理器
* 管理应用的权限使用记录和隐私信息展示
*/
class PrivacyBoardManager {
private atManager: abilityAccessCtrl.AtManager | null = null;
private usageRecords: Map<string, PermissionUsageRecord> = new Map();
/**
* 初始化
*/
init(): void {
this.atManager = abilityAccessCtrl.createAtManager();
console.info('[Privacy] 隐私看板管理器初始化成功');
}
/**
* 记录权限使用
* 在每次使用敏感权限时调用,确保隐私看板数据准确
* @param permission 权限名称
* @param isForeground 是否前台使用
*/
recordPermissionUsage(permission: string, isForeground: boolean = true): void {
const existing = this.usageRecords.get(permission);
const now = Date.now();
if (existing) {
existing.lastUsedTime = now;
existing.useCount += 1;
existing.isForeground = isForeground;
} else {
this.usageRecords.set(permission, {
permission: permission,
permissionName: this.getPermissionDisplayName(permission),
lastUsedTime: now,
useCount: 1,
isForeground: isForeground,
});
}
console.info(`[Privacy] 记录权限使用: ${permission}, 前台=${isForeground}`);
}
/**
* 获取所有权限使用记录
*/
getUsageRecords(): PermissionUsageRecord[] {
return Array.from(this.usageRecords.values())
.sort((a, b) => b.lastUsedTime - a.lastUsedTime); // 按时间倒序
}
/**
* 获取高风险权限使用统计
* 敏感权限:相机、麦克风、位置、通讯录
*/
getSensitiveUsageSummary(): Record<string, number> {
const sensitivePermissions = [
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE',
'ohos.permission.LOCATION',
'ohos.permission.APPROXIMATELY_LOCATION',
'ohos.permission.READ_CONTACTS',
];
const summary: Record<string, number> = {};
for (const perm of sensitivePermissions) {
const record = this.usageRecords.get(perm);
summary[this.getPermissionDisplayName(perm)] = record ? record.useCount : 0;
}
return summary;
}
/**
* 检查权限是否已授权
*/
async checkPermission(tokenID: number, permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
if (!this.atManager) return abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
try {
const result = await this.atManager.checkAccessToken(tokenID, permission);
return result;
} catch (err) {
console.error(`[Privacy] 检查权限失败: ${JSON.stringify(err)}`);
return abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
}
}
/**
* 请求权限(带隐私说明)
* @param context 上下文
* @param permissions 权限列表
* @param reason 申请原因(展示给用户)
*/
async requestPermissionsWithReason(
context: Context,
permissions: Array<Permissions>,
reason: string
): Promise<Record<string, boolean>> {
if (!this.atManager) return {};
const results: Record<string, boolean> = {};
try {
// 逐个请求权限,每个都附带原因说明
for (const permission of permissions) {
console.info(`[Privacy] 请求权限: ${permission}, 原因: ${reason}`);
const result = await this.atManager.requestPermissionsFromUser(context, [permission]);
const granted = result.authResults[0] === 0;
results[permission] = granted;
if (granted) {
this.recordPermissionUsage(permission, true);
}
}
} catch (err) {
console.error(`[Privacy] 请求权限失败: ${JSON.stringify(err)}`);
}
return results;
}
/**
* 获取权限显示名称
*/
private getPermissionDisplayName(permission: string): string {
const nameMap: Record<string, string> = {
'ohos.permission.CAMERA': '📷 相机',
'ohos.permission.MICROPHONE': '🎤 麦克风',
'ohos.permission.LOCATION': '📍 精确位置',
'ohos.permission.APPROXIMATELY_LOCATION': '📍 大致位置',
'ohos.permission.READ_CONTACTS': '👥 读取通讯录',
'ohos.permission.WRITE_CONTACTS': '👥 写入通讯录',
'ohos.permission.READ_CALENDAR': '📅 读取日历',
'ohos.permission.READ_MEDIA': '🖼️ 读取媒体文件',
'ohos.permission.WRITE_MEDIA': '🖼️ 写入媒体文件',
'ohos.permission.ACTIVITY_MOTION': '🏃 运动数据',
'ohos.permission.READ_HEALTH_DATA': '❤️ 健康数据',
};
return nameMap[permission] || permission;
}
}
/**
* 隐私看板页面
*/
@Entry
@Component
struct PrivacyBoardPage {
@State usageRecords: PermissionUsageRecord[] = [];
@State sensitiveSummary: Record<string, number> = {};
private boardManager: PrivacyBoardManager = new PrivacyBoardManager();
aboutToAppear(): void {
this.boardManager.init();
// 模拟记录一些权限使用
this.boardManager.recordPermissionUsage('ohos.permission.CAMERA', true);
this.boardManager.recordPermissionUsage('ohos.permission.LOCATION', true);
this.boardManager.recordPermissionUsage('ohos.permission.LOCATION', false);
this.boardManager.recordPermissionUsage('ohos.permission.MICROPHONE', true);
// 获取记录
this.usageRecords = this.boardManager.getUsageRecords();
this.sensitiveSummary = this.boardManager.getSensitiveUsageSummary();
}
build() {
Scroll() {
Column() {
Text('🛡️ 隐私看板')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
Text('查看本应用的权限使用情况')
.fontSize(14)
.fontColor('#666')
.margin({ bottom: 24 })
// 敏感权限使用概览
Column() {
Text('敏感权限使用统计')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 12 })
ForEach(Object.entries(this.sensitiveSummary), ([name, count]: [string, number]) => {
Row() {
Text(name)
.fontSize(14)
.layoutWeight(1)
Text(`${count} 次`)
.fontSize(14)
.fontColor(count > 0 ? '#FF9800' : '#999')
.fontWeight(count > 0 ? FontWeight.Bold : FontWeight.Normal)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.borderRadius(8)
})
}
.width('100%')
.padding(16)
.backgroundColor('#FFF3E0')
.borderRadius(12)
.margin({ bottom: 20 })
// 详细使用记录
Column() {
Text('权限使用记录')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 12 })
ForEach(this.usageRecords, (record: PermissionUsageRecord) => {
Row() {
Column() {
Text(record.permissionName)
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(this.formatTime(record.lastUsedTime))
.fontSize(12)
.fontColor('#999')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Column() {
Text(`${record.useCount} 次`)
.fontSize(14)
.fontColor('#4CAF50')
Text(record.isForeground ? '前台' : '后台')
.fontSize(11)
.fontColor(record.isForeground ? '#4CAF50' : '#FF9800')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.End)
}
.width('100%')
.padding(12)
.backgroundColor('#fff')
.borderRadius(8)
.margin({ bottom: 8 })
})
}
.width('100%')
.padding(16)
.backgroundColor('#f5f5f5')
.borderRadius(12)
}
.padding(20)
}
.width('100%')
.height('100%')
}
/**
* 格式化时间
*/
private formatTime(timestamp: number): string {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${date.getMonth() + 1}/${date.getDate()} ${hours}:${minutes}`;
}
}
export { PrivacyBoardManager, PermissionUsageRecord };
3.2 广告ID管理与数据删除请求:尊重用户的选择权
广告ID和"被遗忘权"是隐私合规的两个关键要求。用户应该能控制广告追踪,也应该能删除自己的数据。
import { identifier } from '@kit.ArkData';
import { preferences } from '@kit.ArkData';
/**
* 广告ID管理器
* 管理广告标识符的获取、重置和限制追踪
*/
class AdvertisingIdManager {
private prefs: preferences.Preferences | null = null;
/**
* 初始化
*/
async init(context: Context): Promise<void> {
this.prefs = await preferences.getPreferences(context, 'ad_privacy');
}
/**
* 获取广告ID
* 注意:必须尊重用户的"限制广告追踪"选择
*/
async getAdvertisingId(): Promise<string> {
try {
// 先检查用户是否限制了广告追踪
const isLimitAdTracking = await this.isLimitAdTracking();
if (isLimitAdTracking) {
console.info('[AdId] 用户已限制广告追踪,不返回广告ID');
return ''; // 返回空字符串,不返回任何标识符
}
// 获取广告ID
const adId = await identifier.getAdvertisingId();
console.info(`[AdId] 获取广告ID: ${adId ? '成功' : '为空'}`);
return adId || '';
} catch (err) {
console.error(`[AdId] 获取广告ID失败: ${JSON.stringify(err)}`);
return '';
}
}
/**
* 检查用户是否限制了广告追踪
*/
async isLimitAdTracking(): Promise<boolean> {
try {
const isLimited = await identifier.isLimitAdTrackingEnabled();
console.info(`[AdId] 限制广告追踪: ${isLimited ? '是' : '否'}`);
return isLimited;
} catch (err) {
console.error(`[AdId] 检查限制追踪失败: ${JSON.stringify(err)}`);
// 出错时默认尊重隐私,返回 true
return true;
}
}
/**
* 获取广告ID使用记录
* 用于隐私看板展示
*/
async getAdIdUsageLog(): Promise<Record<string, string>> {
if (!this.prefs) return {};
const lastAccess = this.prefs.getStringSync('ad_id_last_access', '从未');
const accessCount = this.prefs.getNumberSync('ad_id_access_count', 0);
return {
'最后访问时间': lastAccess,
'访问次数': `${accessCount} 次`,
};
}
/**
* 记录广告ID访问(用于透明度)
*/
private async logAdIdAccess(): Promise<void> {
if (!this.prefs) return;
const now = new Date().toLocaleString('zh-CN');
await this.prefs.put('ad_id_last_access', now);
const count = this.prefs.getNumberSync('ad_id_access_count', 0);
await this.prefs.put('ad_id_access_count', count + 1);
await this.prefs.flush();
}
}
/**
* 数据删除请求管理器
* 响应用户的"被遗忘权"请求
*/
class DataDeletionManager {
private prefs: preferences.Preferences | null = null;
/**
* 初始化
*/
async init(context: Context): Promise<void> {
this.prefs = await preferences.getPreferences(context, 'data_deletion');
}
/**
* 提交数据删除请求
* @param dataType 要删除的数据类型
* @param reason 删除原因
*/
async submitDeletionRequest(
dataType: 'all' | 'profile' | 'history' | 'cache',
reason: string
): Promise<{ requestId: string; estimatedDays: number }> {
// 生成请求ID
const requestId = `DEL_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
// 估算处理时间
const estimatedDays = dataType === 'all' ? 7 : 3;
// 保存请求记录
if (this.prefs) {
await this.prefs.put(`request_${requestId}`, JSON.stringify({
requestId,
dataType,
reason,
submitTime: Date.now(),
status: 'pending',
}));
await this.prefs.flush();
}
console.info(`[Deletion] 数据删除请求已提交: ${requestId}, 类型=${dataType}`);
// 在实际项目中,这里应该调用后端 API 提交删除请求
// 此处模拟本地处理
await this.processDeletion(dataType);
return { requestId, estimatedDays };
}
/**
* 处理数据删除
* 根据数据类型执行对应的删除操作
*/
private async processDeletion(dataType: string): Promise<void> {
switch (dataType) {
case 'all':
// 删除所有用户数据
await this.deleteAllUserData();
break;
case 'profile':
// 仅删除个人资料
await this.deleteProfileData();
break;
case 'history':
// 仅删除浏览/使用历史
await this.deleteHistoryData();
break;
case 'cache':
// 仅删除缓存
await this.deleteCacheData();
break;
}
}
/**
* 删除所有用户数据
*/
private async deleteAllUserData(): Promise<void> {
console.info('[Deletion] 开始删除所有用户数据');
// 1. 清除偏好设置
if (this.prefs) {
await this.prefs.clear();
await this.prefs.flush();
}
// 2. 清除本地数据库
// 在实际项目中,删除 relationalStore 数据库文件
// 3. 清除文件缓存
// 在实际项目中,删除应用沙箱内的缓存文件
// 4. 通知服务端删除云端数据
// 在实际项目中,调用后端 API 删除服务端数据
console.info('[Deletion] 所有用户数据删除完成');
}
/**
* 删除个人资料数据
*/
private async deleteProfileData(): Promise<void> {
console.info('[Deletion] 删除个人资料数据');
// 删除昵称、头像、生日等个人资料
}
/**
* 删除历史数据
*/
private async deleteHistoryData(): Promise<void> {
console.info('[Deletion] 删除浏览/使用历史');
// 删除搜索历史、浏览记录、使用记录
}
/**
* 删除缓存数据
*/
private async deleteCacheData(): Promise<void> {
console.info('[Deletion] 删除缓存数据');
// 删除图片缓存、API 缓存、临时文件
}
/**
* 获取删除请求状态
*/
async getDeletionStatus(requestId: string): Promise<string> {
if (!this.prefs) return 'unknown';
const data = this.prefs.getStringSync(`request_${requestId}`, '');
if (!data) return 'not_found';
try {
const record = JSON.parse(data);
return record.status;
} catch {
return 'error';
}
}
}
/**
* 隐私管理页面
*/
@Entry
@Component
struct PrivacyManagementPage {
@State isLimitAdTracking: boolean = false;
@State adIdStatus: string = '未获取';
@State deletionStatus: string = '';
@State showDeletionConfirm: boolean = false;
private adIdManager: AdvertisingIdManager = new AdvertisingIdManager();
private deletionManager: DataDeletionManager = new DataDeletionManager();
async aboutToAppear(): Promise<void> {
const context = getContext(this);
await this.adIdManager.init(context);
await this.deletionManager.init(context);
// 检查广告追踪状态
this.isLimitAdTracking = await this.adIdManager.isLimitAdTracking();
this.adIdStatus = this.isLimitAdTracking ? '已限制追踪' : '允许追踪';
}
build() {
Scroll() {
Column() {
Text('🔒 隐私管理')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 24 })
// 广告ID管理
Column() {
Text('📢 广告追踪设置')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 12 })
Row() {
Text('限制广告追踪')
.fontSize(14)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.isLimitAdTracking })
.onChange(async (isOn: boolean) => {
// 注意:限制广告追踪是系统级设置
// 应用只能读取状态,不能直接修改
// 这里引导用户去系统设置
this.isLimitAdTracking = isOn;
if (isOn) {
this.adIdStatus = '已限制追踪';
} else {
this.adIdStatus = '允许追踪';
}
})
}
.width('100%')
Row() {
Text('当前状态:')
.fontSize(12)
.fontColor('#666')
Text(this.adIdStatus)
.fontSize(12)
.fontColor(this.isLimitAdTracking ? '#4CAF50' : '#FF9800')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.margin({ top: 8 })
if (this.isLimitAdTracking) {
Text('✅ 您已选择限制广告追踪,应用不会使用广告ID进行个性化推荐')
.fontSize(12)
.fontColor('#4CAF50')
.margin({ top: 8 })
} else {
Text('⚠️ 广告追踪未限制,应用可能使用广告ID进行个性化推荐')
.fontSize(12)
.fontColor('#FF9800')
.margin({ top: 8 })
}
}
.width('100%')
.padding(16)
.backgroundColor('#E8F5E9')
.borderRadius(12)
.margin({ bottom: 20 })
// 数据删除请求
Column() {
Text('🗑️ 数据管理')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 12 })
Text('根据相关法律法规,您有权要求删除您的个人数据。')
.fontSize(12)
.fontColor('#666')
.margin({ bottom: 16 })
// 删除选项
Column() {
this.DeletionOption('仅删除缓存数据', 'cache', '清除本地缓存,不影响账号数据')
this.DeletionOption('删除浏览历史', 'history', '清除搜索和浏览记录')
this.DeletionOption('删除个人资料', 'profile', '清除昵称、头像等个人资料')
this.DeletionOption('删除所有数据', 'all', '⚠️ 此操作不可恢复,将删除所有数据')
}
}
.width('100%')
.padding(16)
.backgroundColor('#FFF3E0')
.borderRadius(12)
.margin({ bottom: 20 })
// 删除确认弹窗
if (this.showDeletionConfirm) {
Column() {
Text('⚠️ 确认删除')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 12 })
Text(this.deletionStatus)
.fontSize(14)
.fontColor('#F44336')
.margin({ bottom: 16 })
Row() {
Button('取消')
.backgroundColor('#e0e0e0')
.fontColor('#333')
.onClick(() => {
this.showDeletionConfirm = false;
})
Button('确认删除')
.backgroundColor('#F44336')
.onClick(async () => {
const result = await this.deletionManager.submitDeletionRequest('all', '用户主动请求');
this.deletionStatus = `请求已提交,预计 ${result.estimatedDays} 天内完成`;
this.showDeletionConfirm = false;
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
}
.width('80%')
.padding(20)
.backgroundColor('#fff')
.borderRadius(16)
.shadow({ radius: 20, color: '#40000000' })
}
}
.padding(20)
}
.width('100%')
.height('100%')
}
@Builder
DeletionOption(title: string, type: string, desc: string) {
Row() {
Column() {
Text(title)
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(desc)
.fontSize(11)
.fontColor('#999')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Button('删除')
.fontSize(12)
.height(28)
.backgroundColor(type === 'all' ? '#F44336' : '#FF9800')
.onClick(async () => {
if (type === 'all') {
this.deletionStatus = '确定要删除所有数据吗?此操作不可恢复!';
this.showDeletionConfirm = true;
} else {
await this.deletionManager.submitDeletionRequest(
type as 'all' | 'profile' | 'history' | 'cache',
'用户主动请求'
);
}
})
}
.width('100%')
.padding(12)
.backgroundColor('#fff')
.borderRadius(8)
.margin({ bottom: 8 })
}
}
export { AdvertisingIdManager, DataDeletionManager };
3.3 隐私合规开发:最小权限与安全存储的最佳实践
隐私合规不是"加个开关"就完事的,它贯穿整个开发流程。以下是一个完整的隐私合规开发模板。
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { cipherFramework } from '@kit.CryptoArchitectureKit';
import { preferences } from '@kit.ArkData';
/**
* 隐私合规开发工具集
* 提供权限管理、数据加密、隐私声明等能力
*/
class PrivacyComplianceKit {
private atManager: abilityAccessCtrl.AtManager | null = null;
/**
* 权限分级定义
* 根据敏感程度分级,指导权限申请策略
*/
static readonly PERMISSION_LEVELS: Record<string, { level: string; risk: string; alternative: string }> = {
'ohos.permission.CAMERA': {
level: '高',
risk: '可获取用户影像',
alternative: '让用户从相册选择图片',
},
'ohos.permission.MICROPHONE': {
level: '高',
risk: '可获取用户声音',
alternative: '让用户输入文字代替语音',
},
'ohos.permission.LOCATION': {
level: '高',
risk: '可获取用户精确位置',
alternative: '使用大致位置或手动选择城市',
},
'ohos.permission.APPROXIMATELY_LOCATION': {
level: '中',
risk: '可获取用户大致位置',
alternative: '让用户手动选择城市',
},
'ohos.permission.READ_CONTACTS': {
level: '高',
risk: '可读取用户通讯录',
alternative: '让用户手动输入联系人信息',
},
'ohos.permission.READ_MEDIA': {
level: '中',
risk: '可读取用户媒体文件',
alternative: '使用系统选择器让用户选择文件',
},
};
init(): void {
this.atManager = abilityAccessCtrl.createAtManager();
}
/**
* 安全的权限请求流程
* 1. 检查是否已有权限
* 2. 检查是否应该展示申请理由
* 3. 申请权限
* 4. 处理授权结果
*/
async requestPermissionSafely(
context: Context,
permission: Permissions,
reason: string
): Promise<{ granted: boolean; shouldShowRationale: boolean }> {
if (!this.atManager) {
return { granted: false, shouldShowRationale: false };
}
try {
// 第一步:检查是否已授权
const tokenID = await this.getTokenId(context);
const status = await this.atManager.checkAccessToken(tokenID, permission);
if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
console.info(`[Compliance] 权限已授予: ${permission}`);
return { granted: true, shouldShowRationale: false };
}
// 第二步:申请权限
console.info(`[Compliance] 请求权限: ${permission}, 原因: ${reason}`);
const result = await this.atManager.requestPermissionsFromUser(context, [permission]);
const granted = result.authResults[0] === 0;
// 第三步:判断是否应该展示申请理由
// 如果用户选择了"拒绝且不再询问",shouldShowRationale 为 false
const shouldShowRationale = !granted && result.dialogShownResults?.[0] !== false;
if (!granted) {
// 用户拒绝,提供替代方案
const permInfo = PrivacyComplianceKit.PERMISSION_LEVELS[permission];
if (permInfo) {
console.info(`[Compliance] 权限被拒绝,替代方案: ${permInfo.alternative}`);
}
}
return { granted, shouldShowRationale };
} catch (err) {
console.error(`[Compliance] 权限请求异常: ${JSON.stringify(err)}`);
return { granted: false, shouldShowRationale: false };
}
}
/**
* 获取应用 TokenID
*/
private async getTokenId(context: Context): Promise<number> {
const bundleInfo = await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
);
return bundleInfo.appInfo.accessTokenId;
}
/**
* 敏感数据加密存储
* 隐私数据不应明文存储
*/
async encryptAndStore(
context: Context,
key: string,
plainText: string,
secretKey: string
): Promise<boolean> {
try {
// 使用 AES 加密
const cipher = cipherFramework.createCipher('AES256|GCM|PKCS7');
const symKeyGenerator = cipherFramework.createSymKeyGenerator('AES256');
// 生成密钥
const keyData = new Uint8Array(Buffer.from(secretKey.padEnd(32, '0'), 'utf-8'));
const symKey = await symKeyGenerator.convertKey({ data: keyData });
// 初始化加密
await cipher.init(cipherFramework.CryptoMode.ENCRYPT_MODE, symKey, null);
// 加密
const input = { data: new Uint8Array(Buffer.from(plainText, 'utf-8')) };
const encrypted = await cipher.doFinal(input);
// 存储加密后的数据
const prefs = await preferences.getPreferences(context, 'encrypted_data');
await prefs.put(key, Buffer.from(encrypted.data).toString('base64'));
await prefs.flush();
console.info(`[Compliance] 敏感数据已加密存储: ${key}`);
return true;
} catch (err) {
console.error(`[Compliance] 加密存储失败: ${JSON.stringify(err)}`);
return false;
}
}
/**
* 生成隐私声明文本
* 用于应用首次启动时的隐私弹窗
*/
getPrivacyStatement(): string {
return `隐私声明
本应用重视您的隐私保护,承诺:
1. 最小权限原则:仅申请实现功能所必需的权限
2. 透明度原则:每次使用敏感权限都会告知您
3. 数据最小化:仅收集实现功能所必需的数据
4. 安全存储:您的敏感数据均经过加密存储
5. 被遗忘权:您可以随时请求删除您的所有数据
本应用使用的敏感权限及用途:
• 📷 相机 - 用于扫描二维码
• 📍 位置 - 用于展示附近的服务点
• 📢 广告ID - 用于个性化推荐(您可以在设置中关闭)
您可以随时在"设置 > 隐私管理"中查看和管理您的隐私选项。`;
}
}
/**
* 隐私合规首次启动页面
*/
@Entry
@Component
struct PrivacyConsentPage {
@State hasAgreed: boolean = false;
@State showPrivacyDialog: boolean = true;
private complianceKit: PrivacyComplianceKit = new PrivacyComplianceKit();
async aboutToAppear(): Promise<void> {
this.complianceKit.init();
// 检查是否已同意隐私声明
const context = getContext(this);
const prefs = await preferences.getPreferences(context, 'privacy');
this.hasAgreed = prefs.getBooleanSync('privacy_agreed', false);
if (this.hasAgreed) {
this.showPrivacyDialog = false;
}
}
build() {
Stack() {
// 主内容
Column() {
Text('欢迎使用本应用')
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
// 隐私声明弹窗
if (this.showPrivacyDialog) {
Column() {
Text('📋 隐私声明')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
Scroll() {
Text(this.complianceKit.getPrivacyStatement())
.fontSize(13)
.lineHeight(22)
.fontColor('#333')
}
.height(300)
.width('100%')
.margin({ bottom: 16 })
Row() {
Button('不同意')
.backgroundColor('#e0e0e0')
.fontColor('#333')
.layoutWeight(1)
.onClick(() => {
// 用户不同意,退出应用
console.warn('[Privacy] 用户不同意隐私声明,应用退出');
})
Button('同意并继续')
.backgroundColor('#4CAF50')
.fontColor('#fff')
.layoutWeight(1)
.onClick(async () => {
this.hasAgreed = true;
this.showPrivacyDialog = false;
// 保存同意记录
const context = getContext(this);
const prefs = await preferences.getPreferences(context, 'privacy');
await prefs.put('privacy_agreed', true);
await prefs.put('privacy_agreed_time', Date.now());
await prefs.flush();
console.info('[Privacy] 用户已同意隐私声明');
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
.width('90%')
.padding(20)
.backgroundColor('#fff')
.borderRadius(16)
.shadow({ radius: 20, color: '#40000000' })
}
}
.width('100%')
.height('100%')
}
}
export { PrivacyComplianceKit };
四、踩坑与注意事项
4.1 隐私声明必须在功能之前展示
坑:应用先进入主界面,再弹隐私声明——审核直接被拒。
解:隐私声明必须在应用启动时、用户使用任何功能之前展示。用户点击"同意"后才能继续。
4.2 权限被拒绝后的降级处理
坑:用户拒绝权限后,应用直接崩溃或功能完全不可用。
解:每个敏感权限都应该有替代方案。相机被拒绝?让用户从相册选择。位置被拒绝?让用户手动输入城市。
// ✅ 正确:权限被拒后提供替代方案
async function getLocation(): Promise<string> {
const granted = await requestPermission('ohos.permission.LOCATION');
if (granted) {
return await getCurrentLocation();
} else {
// 替代方案:让用户手动选择城市
return await showCityPicker();
}
}
4.3 广告ID的限制追踪检查
坑:不检查限制广告追踪状态就直接使用广告ID,导致隐私合规问题。
解:每次使用广告ID前,必须先检查 isLimitAdTrackingEnabled()。如果用户限制了追踪,不能使用任何标识符替代。
4.4 数据删除不能"留尾巴"
坑:用户请求数据删除,但只删了本地数据,服务端和备份中的数据没删。
解:数据删除必须覆盖所有存储位置——本地、服务端、备份、CDN 缓存。建议建立删除检查清单。
4.5 敏感数据不能明文存储
坑:将身份证号、密码等敏感数据明文存在 Preferences 中。
解:使用 cipherFramework 进行加密存储,密钥使用系统安全存储(HUKS)。
五、HarmonyOS 6 适配
5.1 API 变化一览
| 变化项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 隐私看板 | 基础权限记录 | 新增实时权限使用指示器(状态栏图标) |
| 广告ID | identifier 模块 | 新增 OAID(开放广告标识符)合规要求 |
| 数据删除 | 无标准接口 | 新增 DataDeletionRequest 标准化接口 |
| 权限申请 | requestPermissionsFromUser | 新增"仅本次授权"选项 |
| 隐私声明 | 无标准格式 | 新增 PrivacyStatement 标准模板 |
| 生物识别 | 基础指纹 | 新增面部识别隐私保护 |
5.2 迁移指南
// HarmonyOS 5:自定义数据删除流程
async function handleDeletion() {
// 自行实现删除逻辑
await deleteLocalData();
await callServerDeletionAPI();
}
// HarmonyOS 6:使用标准化接口
import { privacy } from '@kit.AbilityKit';
const deletionManager = privacy.createDataDeletionManager();
await deletionManager.submitDeletionRequest({
dataType: privacy.DeletionDataType.ALL,
includeCloudData: true, // 同时删除云端数据
includeBackups: true, // 同时删除备份数据
});
5.3 注意事项
- “仅本次授权”:HarmonyOS 6 新增的授权选项,应用退出后自动撤销。需要在每次启动时重新申请。
- OAID 合规:如果应用使用广告ID,必须提供"限制广告追踪"的入口,并在用户限制后停止使用。
- 实时指示器:系统会在状态栏显示应用正在使用敏感权限的图标,开发者无需额外处理,但应确保权限使用行为合理。
六、总结
mindmap
root((隐私设置))
隐私看板
权限使用概览
敏感行为记录
风险提醒
实时指示器(HarmonyOS 6)
权限使用记录
使用时间/时长
访问频率
前后台状态
透明化展示
广告ID管理
获取广告ID
限制广告追踪检查
广告ID使用记录
OAID 合规
数据删除请求
分级删除选项
全链路删除
删除确认机制
标准化接口(HarmonyOS 6)
隐私合规开发
最小权限原则
透明度原则
数据最小化
安全加密存储
隐私声明弹窗
降级替代方案
注意事项
隐私声明先于功能
权限拒绝需降级
广告ID检查限制追踪
数据删除不留尾巴
敏感数据加密存储
| 知识点 | 要点 |
|---|---|
| 隐私看板 | 记录每次权限使用,让用户看到"谁在用什么" |
| 权限使用记录 | 记录时间、频率、前后台状态,确保透明度 |
| 广告ID | 使用前检查限制追踪状态,尊重用户选择 |
| 数据删除 | 覆盖本地/服务端/备份全链路,不留尾巴 |
| 最小权限 | 能不用就不用,能降级就降级,每个权限都有替代方案 |
| HarmonyOS 6 | 仅本次授权、标准化删除接口、实时权限指示器 |
隐私保护就像给用户的数据上锁——不是因为你信不过别人,而是因为这是对用户最基本的尊重。在 HarmonyOS 生态中,隐私合规不是可选项,而是必修课。掌握了这些,你的应用才能赢得用户的信任,走得更远。
- 点赞
- 收藏
- 关注作者
评论(0)