HarmonyOS 隐私合规实战:权限申请、隐私声明与数据处理
很多 HarmonyOS 应用第一次提审,功能没崩、页面也挺漂亮,最后却卡在了“隐私合规”。
更让人摸不着头脑的是:隐私政策明明写了,首次启动也弹窗了,为什么还是被驳回?
因为隐私合规从来不只是一个弹窗。页面上怎么说、代码里怎么做、第三方 SDK 实际收集了什么,三者必须对得上。
这篇文章不照着条款念,也不堆法律术语。我们用开发者最熟悉的方式,从一次权限申请开始,把隐私声明、授权时机、SDK 初始化、日志脱敏、数据删除和提审自查完整走一遍。
文中的示例采用 ArkTS 和 Stage 模型。不同 HarmonyOS SDK/API 版本的接口可能略有变化,请以当前工程的类型提示和最新开发文档为准。
一、先记住这 5 句话,能避开大部分坑
如果暂时没时间读全文,先记住下面五点:
- **声明了权限,不代表可以随时申请。**用户真正使用功能时再申请。
- 用户点同意之前,不要初始化非必要的统计、广告、画像等 SDK。
- 隐私政策写了什么,代码就只能做什么;代码做了什么,隐私政策就必须如实说明。
- 用户拒绝非必要权限后,应用不能闪退、卡死或无限弹窗。
- 第三方 SDK 的行为,也算应用的行为。“这是 SDK 自动采集的”通常不是合格解释。
可以把隐私合规理解成一个等式:
隐私合规 = 清楚告知 + 用户选择 + 最小收集 + 安全处理 + 可以撤回
少一项,整个链路都可能出问题。
二、隐私弹窗不是“免责按钮”
先看一个非常典型的错误流程:
应用启动
↓
初始化统计 SDK、广告 SDK、推送 SDK
↓
读取设备信息或生成标识
↓
弹出隐私政策
↓
用户点击同意
表面上看,应用确实弹了隐私政策;但在用户同意之前,SDK 已经运行,甚至可能已经发出了网络请求。弹窗出现得再漂亮,也改变不了“先处理、后告知”的事实。
正确顺序应该是:
应用启动
↓
读取本地的隐私同意状态
├─ 已同意:进入应用,初始化已披露的服务
└─ 未同意:展示隐私说明
├─ 同意:记录状态,再初始化相关服务
└─ 不同意:退出或进入不采集非必要信息的基础模式
这里有三个容易忽略的细节:
- 弹窗中的“隐私政策”和“用户协议”应该可以点击并完整阅读;
- 同意按钮不能默认勾选,也不能用几乎看不见的颜色弱化“不同意”;
- 用户同意后才初始化的逻辑,不能因为页面重构又偷偷挪回启动阶段。
所以,与其把隐私合规散落在十几个页面里,不如做一个统一的“隐私启动闸门”。
三、权限申请:不是能申请,而是该不该申请
HarmonyOS 提供了系统权限机制,但系统允许申请某项权限,不代表你的业务就有理由申请。
一个天气应用在用户查看“当前位置天气”时申请定位权限,很合理;一个壁纸应用刚启动就申请麦克风、通讯录和位置,审核员和用户都会问一句:你到底想干什么?
1. 先做一张权限盘点表
写代码前,先把权限和功能的关系列出来:
| 权限或能力 | 使用场景 | 是否核心功能 | 能否替代 | 申请时机 |
|---|---|---|---|---|
| 相机 | 扫描二维码 | 否 | 可以手动输入编号 | 点击“扫一扫”后 |
| 定位 | 查询附近门店 | 否 | 可以手动选择城市 | 点击“附近门店”后 |
| 麦克风 | 语音输入 | 否 | 可以键盘输入 | 点击麦克风按钮后 |
| 通知 | 到期提醒 | 否 | 应用内消息中心 | 用户主动开启提醒时 |
如果“使用场景”这一栏写不清楚,这个权限很可能不该申请。
如果存在不需要敏感权限的系统能力,也应该优先考虑。例如,用户主动选择一张图片时,优先研究系统选择器能否满足需求,不要一上来就申请整个媒体库的访问权限。
2. 在 module.json5 中正确声明
以扫码功能需要相机权限为例:
{
"module": {
"name": "entry",
"type": "entry",
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:camera_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
对应的权限说明不要写成一句没有信息量的“用于相机功能”,可以这样表达:
{
"string": [
{
"name": "camera_permission_reason",
"value": "用于扫描设备二维码,仅在你主动使用扫一扫功能时调用相机"
}
]
}
对比一下:
不推荐:需要相机权限,请允许。
推荐:用于扫描设备二维码,仅在你点击“扫一扫”后调用相机。
第二句话回答了三个问题:**做什么、什么时候用、为什么需要。**这才是有效说明。
3. 在真正需要时动态申请
不要在 aboutToAppear() 中一股脑申请所有权限。用户点击“扫一扫”时,再请求相机权限:
import { abilityAccessCtrl, common } from '@kit.AbilityKit'
const CAMERA_PERMISSION = 'ohos.permission.CAMERA'
async function requestCameraPermission(
context: common.UIAbilityContext
): Promise<boolean> {
const accessManager = abilityAccessCtrl.createAtManager()
try {
const result = await accessManager.requestPermissionsFromUser(
context,
[CAMERA_PERMISSION]
)
// 当前常用 API 中,0 表示授权成功。
// 正式项目建议结合当前 SDK 的 GrantStatus 枚举判断。
return result.authResults.length > 0 && result.authResults[0] === 0
} catch (error) {
console.error(`request camera permission failed: ${String(error)}`)
return false
}
}
在页面中调用:
import { common } from '@kit.AbilityKit'
import { promptAction } from '@kit.ArkUI'
@Component
struct ScannerEntry {
private async openScanner(): Promise<void> {
const context = getContext(this) as common.UIAbilityContext
const granted = await requestCameraPermission(context)
if (granted) {
// router.pushUrl({ url: 'pages/Scanner' })
return
}
promptAction.showToast({
message: '没有相机权限,可返回后手动输入设备编号'
})
}
build() {
Column({ space: 16 }) {
Button('扫一扫')
.onClick(() => this.openScanner())
Button('手动输入设备编号')
.onClick(() => {
// 进入不依赖相机权限的替代流程
})
}
}
}
这段代码最重要的不是 API,而是产品逻辑:
- 用户点了相关功能,才看到权限请求;
- 授权成功,进入扫码页面;
- 用户拒绝,不崩溃,也不反复弹窗;
- 提供“手动输入”作为替代方案。
这就是隐私合规里经常提到的“最小必要”和“可选择”。
4. 权限被拒绝后,千万别死循环
下面这种写法看似执着,实际上很容易让用户崩溃:
// 错误示意:拒绝后立即再次申请
while (!granted) {
granted = await requestCameraPermission(context)
}
正确思路是:
- 第一次拒绝:解释影响,提供替代操作;
- 用户以后再次点击相关功能:可以重新触发合理的申请流程;
- 如果系统不再弹出授权框:引导用户前往系统设置,但不要强迫;
- 与当前功能无关的页面:不要反复提醒。
用户拒绝的是一项权限,不是拒绝使用你的整个应用。
四、把“同意隐私政策”真正落到代码里
隐私同意状态通常需要持久化,否则用户每次打开应用都要重新选择。可以使用 Preferences 保存状态。
1. 封装同意状态存储
import { preferences } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'
const PRIVACY_STORE = 'privacy_store'
const PRIVACY_AGREED = 'privacy_agreed'
const PRIVACY_VERSION = 'privacy_version'
const CURRENT_PRIVACY_VERSION = '2026.09'
export class PrivacyStore {
static async hasAgreed(context: common.Context): Promise<boolean> {
const store = await preferences.getPreferences(context, {
name: PRIVACY_STORE
})
const agreed = await store.get(PRIVACY_AGREED, false) as boolean
const version = await store.get(PRIVACY_VERSION, '') as string
// 隐私政策发生重要变化时,可以根据版本重新提示用户
return agreed && version === CURRENT_PRIVACY_VERSION
}
static async agree(context: common.Context): Promise<void> {
const store = await preferences.getPreferences(context, {
name: PRIVACY_STORE
})
await store.put(PRIVACY_AGREED, true)
await store.put(PRIVACY_VERSION, CURRENT_PRIVACY_VERSION)
await store.flush()
}
static async withdraw(context: common.Context): Promise<void> {
const store = await preferences.getPreferences(context, {
name: PRIVACY_STORE
})
await store.put(PRIVACY_AGREED, false)
await store.flush()
}
}
为什么还要保存一个 privacy_version?
因为隐私政策不是永远不变。比如后续版本新增了位置功能,或者接入了新的第三方 SDK,旧版本的同意未必能覆盖新的处理目的。是否需要重新征得同意,应结合具体变化和最新要求判断;从工程角度看,至少要具备版本管理能力。
注意,不能每次普通文案改了一个标点,就强迫用户重新同意。版本机制是为了处理实质变化,不是为了制造弹窗。
2. 建立统一的初始化闸门
import { common } from '@kit.AbilityKit'
export class AppBootstrap {
private static optionalServicesStarted: boolean = false
static startEssentialServices(): void {
// 只启动应用运行必需、且不进行非必要个人信息处理的能力
// 这里依然要结合项目实际情况评估,不能仅靠函数名判断合规
}
static startServicesAfterConsent(): void {
if (this.optionalServicesStarted) {
return
}
this.optionalServicesStarted = true
// 示例:只有确认用户同意,且隐私政策已准确披露后再初始化
// AnalyticsService.init()
// PushService.init()
// AdService.init()
}
static stopOptionalServices(): void {
// 如果相关 SDK 支持停止、注销或清除标识,可在这里统一处理
this.optionalServicesStarted = false
}
static async start(context: common.Context): Promise<boolean> {
this.startEssentialServices()
const agreed = await PrivacyStore.hasAgreed(context)
if (agreed) {
this.startServicesAfterConsent()
}
return agreed
}
}
以后新增 SDK 时,团队不应该随手在 EntryAbility、首页和某个工具类里各初始化一次,而是先回答四个问题:
- 这个 SDK 做什么?
- 它会处理哪些信息?
- 隐私政策是否已经披露?
- 它应该在用户同意前还是同意后初始化?
回答清楚,再把初始化放进对应阶段。
3. 首次启动页面怎么组织?
下面是一个简化的页面思路:
import { common } from '@kit.AbilityKit'
@Entry
@Component
struct Index {
@State private privacyChecked: boolean = false
@State private privacyAgreed: boolean = false
async aboutToAppear(): Promise<void> {
const context = getContext(this) as common.UIAbilityContext
this.privacyAgreed = await AppBootstrap.start(context)
this.privacyChecked = true
}
private async agreeAndContinue(): Promise<void> {
const context = getContext(this) as common.UIAbilityContext
await PrivacyStore.agree(context)
AppBootstrap.startServicesAfterConsent()
this.privacyAgreed = true
}
build() {
Column() {
if (!this.privacyChecked) {
LoadingProgress()
} else if (!this.privacyAgreed) {
PrivacyConsentView({
onAgree: () => this.agreeAndContinue(),
onReject: () => {
// 根据业务选择退出,或进入不处理非必要信息的基础模式
}
})
} else {
HomePage()
}
}
.width('100%')
.height('100%')
}
}
为了突出流程,示例省略了异常处理、并发保护和具体组件实现。实际项目还要处理 Preferences 读取失败、快速重复点击、应用恢复等情况。
还有一个很重要的点:**不要把隐私同意状态上传成一个到处可查的用户标签,除非确实有跨端同步等合理需要。**能在本地完成的判断,就不要为了“方便”额外收集。
五、隐私声明到底该写什么?别再复制模板了
不少团队的隐私政策是从网上找一份模板,把公司名替换掉就上线。这样做最大的问题不是文案难看,而是内容和真实应用完全对不上。
一份能落地的隐私声明,至少应该回答下面这些问题:
1. 我收集什么?
不要只写“可能收集设备信息”,要尽量说清具体类型。例如:
为了完成账号登录,我们会处理你主动填写的手机号。
当你使用“扫一扫”功能时,我们会申请相机权限,用于识别二维码。
相机画面仅用于本地识别;如业务确需上传,应另外准确说明上传目的和范围。
2. 为什么收集?
同一类信息在不同场景中,目的可能完全不同。
例如位置数据:
- 用于展示附近门店;
- 用于配送地址定位;
- 用于个性化广告。
这三个目的不能用一句“改善用户体验”全部带过。目的越模糊,越难证明收集是必要的。
3. 在什么时候收集?
隐私政策中写“使用扫码功能时申请相机”,代码却在首页启动时就申请,这就是声明与行为不一致。
建议产品、开发和合规人员共用一张数据清单:
| 信息/权限 | 处理目的 | 触发页面 | 是否上传 | 保存期限 | 第三方接收方 |
|---|---|---|---|---|---|
| 手机号 | 登录和账号安全 | 登录页 | 是 | 账号存续期间或依法处理 | 短信服务商 |
| 相机 | 扫描二维码 | 扫码页 | 视实际业务填写 | 视实际业务填写 | 无或如实填写 |
| 崩溃日志 | 定位应用异常 | 用户同意后的运行阶段 | 是 | 按实际策略填写 | 崩溃分析服务商 |
这里不要照抄表格里的示例答案。比如你的二维码图片会上传服务器,就不能写“仅本地处理”;你的崩溃分析 SDK 会读取设备标识,也不能只写“收集崩溃时间”。
4. 数据会保存多久?
“我们会永久保存”通常不是一个让人安心的答案,“在实现目的所需的最短时间内保存”如果没有实际删除机制,也可能只是口号。
开发侧至少要能回答:
- 数据存在哪里;
- 依靠什么字段找到它;
- 到期后由谁删除;
- 用户注销时是否同步删除;
- 备份和日志中的数据怎么处理;
- 法律法规要求保留的数据如何隔离。
5. 用户怎么控制自己的数据?
用户应该能够知道如何:
- 查看、更正个人信息;
- 关闭相关权限;
- 撤回隐私同意;
- 删除部分数据;
- 注销账号;
- 联系开发者或投诉反馈。
入口不要藏在连续五层页面之后。常见做法是放在:
我的 → 设置 → 隐私设置
我的 → 设置 → 账号与安全 → 注销账号
只写“如需注销请联系客服”,但客服永远没人回复,这种设计显然经不起实际验证。
六、第三方 SDK:最容易被漏掉的隐私风险
你的代码可能一行设备信息都没读,但只要接入的 SDK 在用户同意前发起了采集,应用仍然可能出现合规问题。
1. 建立 SDK 台账
建议每次发版都维护下面这张表:
| SDK 名称 | 用途 | 可能处理的信息 | 初始化时机 | 是否必要 | 隐私政策是否披露 |
|---|---|---|---|---|---|
| 崩溃分析 SDK | 定位闪退 | 设备信息、崩溃堆栈等 | 用户同意后 | 可评估 | 是/否 |
| 推送 SDK | 消息通知 | 推送标识等 | 按实际能力评估 | 非全部场景必要 | 是/否 |
| 广告 SDK | 展示广告 | 设备及广告相关信息等 | 用户同意后 | 否 | 是/否 |
表中的“可能处理的信息”不能靠猜。应该查看 SDK 的官方隐私说明、版本更新记录和实际运行行为。
2. 升级 SDK 后要重新检查
SDK 从 2.1 升级到 2.2,看起来只是一个小版本,但它可能:
- 新增权限;
- 新增采集字段;
- 改变初始化时机;
- 增加新的子处理方;
- 修改数据保存策略。
所以依赖升级不能只做“编译通过”和“功能正常”测试,还要重新核对隐私说明。
3. 删除 SDK 要删干净
常见情况是:产品说“不接广告了”,开发删除了广告页面,却留下了依赖、初始化代码和配置;或者代码删了,隐私政策仍然写着相关 SDK。
正确的下线动作包括:
删除依赖 → 删除初始化 → 删除相关权限 → 删除配置
→ 检查构建产物 → 更新隐私政策 → 回归网络请求
“应用不再展示广告”和“广告 SDK 已经完全停止工作”是两件不同的事。
七、数据处理:别让日志和缓存毁掉前面的努力
权限和弹窗都做对了,不代表数据处理就安全。手机号、Token、身份证号、定位信息如果被随手打印到日志中,前面的合规工作很可能功亏一篑。
1. 日志要脱敏
错误示例:
console.info(`login success, phone=${phone}, token=${token}`)
手机号和 Token 都没有必要完整出现在日志里。可以封装一个简单的脱敏方法:
function maskPhone(phone: string): string {
if (!/^\d{11}$/.test(phone)) {
return '[invalid-phone]'
}
return `${phone.slice(0, 3)}****${phone.slice(-4)}`
}
function safeError(error: object | string): string {
// 正式项目应使用字段白名单,不要直接 JSON.stringify 整个请求对象
return typeof error === 'string' ? error.slice(0, 200) : '[business-error]'
}
console.info(`login result, phone=${maskPhone(phone)}`)
真正稳妥的思路不是“先把所有信息打出来,再想办法遮住”,而是日志字段白名单:只记录定位问题确实需要的字段。
尤其不要打印:
- 登录密码和短信验证码;
- 完整访问 Token、刷新 Token;
- 身份证号、银行卡号;
- 完整手机号、精确地址;
- 请求头中的认证信息;
- 用户提交的聊天、表单或健康数据全文。
2. 本地存储要分级
Preferences 适合保存普通配置和开关,不代表任何数据都应该往里塞。
可以做一个简单分级:
| 数据 | 推荐思路 |
|---|---|
| 主题颜色、引导页状态 | 普通本地配置 |
| 隐私同意状态 | 本地配置,并做好版本管理 |
| 登录凭证、关键密钥 | 使用适合敏感数据的安全存储和密钥管理能力 |
| 用户业务数据 | 根据必要性、敏感程度、期限设计加密和删除策略 |
不要在代码里硬编码服务端密钥,也不要认为把密钥拆成三段字符串就安全了。客户端最终需要使用的静态秘密,理论上总有被分析的风险;真正的敏感凭证应该尽量由服务端控制,并结合安全存储、短期令牌等机制降低风险。
3. 网络传输要有失败边界
涉及账号和个人信息的请求应使用安全传输,并正确校验证书。不要为了调试方便,在正式包里保留“忽略所有证书错误”一类逻辑。
同时要处理失败场景:
import { http } from '@kit.NetworkKit'
async function deleteUserProfile(): Promise<boolean> {
const request = http.createHttp()
try {
const response = await request.request(
'https://example.com/api/profile',
{
method: http.RequestMethod.DELETE,
connectTimeout: 10000,
readTimeout: 10000
}
)
return response.responseCode >= 200 && response.responseCode < 300
} catch (error) {
console.error(`delete profile failed: ${safeError(error as object)}`)
return false
} finally {
request.destroy()
}
}
删除失败时,不要在客户端直接显示“已删除成功”;应该告诉用户当前状态,并允许重试或查询处理进度。
4. 数据不能“只进不出”
每新增一类数据,开发时就应该同步设计它的退出路径:
采集 → 使用 → 保存 → 到期删除
├─ 用户主动删除
├─ 用户撤回授权
└─ 用户注销账号
如果团队只知道数据怎么写进数据库,却没人知道怎样完整删除,这套功能还没有真正做完。
八、账号注销:一个按钮背后是一整条链路
“设置页加个注销按钮”只是界面完成了,真正的注销至少涉及:
- 确认当前账号身份;
- 清楚说明注销后果;
- 处理未完成订单、余额或其他必要事项;
- 服务端关闭账号并处理相关数据;
- 让现有 Token 和登录状态失效;
- 清理本地缓存及用户数据;
- 给用户明确结果,而不是一直显示“处理中”。
客户端可以把注销后的本地清理集中处理:
class AccountSession {
static async clearAfterCancellation(): Promise<void> {
// AuthTokenStore.clear()
// UserCache.clear()
// SearchHistory.clearByUser()
// AppBootstrap.stopOptionalServices()
// router.clear()
// router.replaceUrl({ url: 'pages/Welcome' })
}
}
上面只是结构示意。真实项目中,哪些数据必须删除、哪些依法需要保留、备份何时清除,要由业务、技术和合规共同确定。
还要区分两个容易混淆的动作:
退出登录:以后还能用原账号重新登录,服务端数据仍然存在。
注销账号:账号关系终止,并按照规则处理相关个人信息。
不能把“退出登录”包装成“注销账号”。
九、一个完整案例:给 App 增加“扫码绑定设备”
假设产品提出一个需求:用户扫描设备上的二维码,把设备绑定到账号。
看起来只是一个扫码页面,但要合规上线,至少需要走完下面的过程。
第一步:判断相机权限是否必要
扫码确实需要相机,但可以提供手动输入设备编号的替代方案。因此:
- 相机权限不是应用启动所必需;
- 用户不授权,仍然能通过手动输入完成业务;
- 权限应该在用户点击“扫一扫”后申请。
第二步:更新权限和隐私说明
module.json5 增加相机权限及具体原因;隐私政策增加相机使用场景,并说明画面是否上传、二维码识别结果如何使用。
第三步:检查第三方扫码库
如果接入第三方扫码 SDK,不能只看它“能不能扫出来”,还要确认:
- SDK 是否发起网络请求;
- 是否收集设备信息;
- 是否保存或上传扫码图片;
- 是否需要在第三方 SDK 清单中披露。
第四步:处理所有授权状态
至少测试:
首次申请并同意
首次申请并拒绝
拒绝后使用手动输入
系统设置中关闭权限后再次扫码
授权后拍摄页面被系统中断
弱网下二维码识别成功但绑定接口失败
第五步:验证日志和服务端
二维码中可能包含设备序列号或绑定密钥,不应该被完整打印到日志。绑定接口也应该校验当前账号是否有权绑定该设备,避免只靠客户端判断。
第六步:写清提审备注
本版本新增“扫码绑定设备”功能。
相机权限仅在用户点击“设备 → 添加设备 → 扫一扫”后申请,
用于识别设备二维码。拒绝权限后,用户可选择“手动输入设备编号”,
不影响应用其他功能。
测试路径:
登录 → 设备 → 添加设备 → 扫一扫
你会发现,一项小功能从需求到上线,隐私合规贯穿了产品、代码、测试、文案和审核说明,而不是上线前让运营临时补一个弹窗。
十、这 8 种做法,看起来合规,其实很危险
1. “隐私政策里都写了,所以权限可以一次性全要”
不对。隐私政策负责告知,系统授权负责让用户在具体场景中选择,两者不能互相替代。
2. “先初始化 SDK,反正用户大概率会同意”
用户还没有选择,“大概率”没有意义。初始化时机应该由实际数据处理行为决定。
3. “权限拒绝后就退出,不然功能不好做”
如果这项权限不是应用基本功能所必需,就应该尽量提供替代方案,而不是把授权做成强制通行证。
4. “我们不保存数据,所以不算收集”
读取、上传、使用和共享都可能属于数据处理行为,不是只有写进数据库才需要关注。
5. “第三方 SDK 有自己的隐私政策,跟我没关系”
SDK 是你选择并集成到应用中的。接入方仍然需要了解它的行为,并完成必要披露和控制。
6. “把权限说明写得越宽泛,以后越省事”
例如“为了向你提供更好的服务,我们可能使用所有设备能力”。这种文案看似覆盖面很广,实际没有清楚解释任何事情。
7. “审核通过了,当前做法肯定永久合规”
应用功能、SDK 和规则都会变化。一次通过只代表那个时间点、那个版本完成了审核,不代表以后可以停止维护。
8. “正式包和测试包代码一样,不用重新测”
Release 构建可能启用不同的服务地址、混淆、签名和资源裁剪。隐私与权限测试一定要基于最终提交的安装包。
十一、如何自己做一次隐私合规测试?
不需要一上来就准备复杂工具,先做一轮“黑盒体验”:
测试一:用户同意前
全新安装并启动应用,在隐私弹窗上停住,检查:
- 是否已经弹出系统权限请求;
- 是否已经初始化非必要 SDK;
- 是否出现与核心展示无关的网络请求;
- 隐私政策链接能否打开;
- 不同意按钮能否正常点击。
测试二:拒绝所有非必要权限
逐个拒绝权限,检查:
- 应用是否崩溃或白屏;
- 页面是否无限加载;
- 是否反复申请;
- 是否说明受影响的具体功能;
- 是否提供合理替代路径。
测试三:权限动态变化
先允许权限,再从系统设置中关闭,然后回到应用。很多应用只处理了“首次申请”,没有处理权限被用户随时收回的情况。
测试四:撤回同意和注销账号
检查入口是否能找到、流程是否真实有效、本地数据是否清理、服务端状态是否更新。
测试五:抓取实际行为进行核对
在具备相应测试条件和授权的环境中,核对应用及第三方 SDK 的网络请求、权限调用和本地存储行为,再与隐私政策逐项对照。
最终应该达到这个状态:
商店隐私信息
= 应用内隐私政策
= 权限用途说明
= 代码真实行为
= 第三方 SDK 真实行为
任何两项对不上,都值得重新检查。
十二、提审前隐私合规清单
建议把下面这张表放进团队的发版流程,每次提交都重新确认。
| 检查项 | 通过标准 |
|---|---|
| 首次启动 | 同意前未启动非必要个人信息处理 |
| 隐私弹窗 | 文案清楚,协议可阅读,同意与不同意均可操作 |
| 权限声明 | 只保留当前版本实际需要的权限 |
| 申请时机 | 用户主动使用相关功能时再申请 |
| 权限文案 | 说明具体功能、目的和使用场景 |
| 拒绝处理 | 不崩溃、不死循环,提供提示或替代方案 |
| 隐私政策 | 与当前应用名称、主体、功能和权限一致 |
| 第三方 SDK | 名称、用途、信息类型和规则如实披露 |
| SDK 初始化 | 按实际数据行为放在正确的同意阶段 |
| 日志 | 不输出密码、验证码、Token 和完整敏感信息 |
| 本地存储 | 普通数据与敏感数据分级处理 |
| 网络传输 | 使用安全连接,不保留跳过证书校验的调试逻辑 |
| 数据期限 | 有明确、可执行的保留与删除策略 |
| 撤回同意 | 入口可找到,撤回后停止对应的非必要处理 |
| 账号注销 | 流程可完成,Token、本地缓存和服务端状态正确处理 |
| 商店资料 | 商店填写的隐私信息与应用实际行为一致 |
| 最终安装包 | 使用提交审核的 Release 包完整回归 |
如果团队人数比较多,可以把责任拆开:
产品:说明功能为什么需要数据,提供替代流程
开发:控制权限和 SDK 时机,做好存储、传输、删除
测试:覆盖拒绝、撤回、弱网和全新安装场景
运营:保证商店资料与当前版本一致
合规/法务:核对隐私政策和具体处理规则
隐私合规不是“最后找一个人签字”,而是每个角色都要把自己的那一段做好。
十三、写在最后
做隐私合规最怕两种极端:
一种是完全不当回事,觉得加个弹窗就能解决;另一种是看到“个人信息”四个字就不敢开发任何功能。
其实核心并不神秘:
需要什么就申请什么,用到时再申请;收集之前说清楚,用户拒绝有退路;数据拿到手后保护好,不需要时及时删除。
把这几件事真正落实到代码和产品流程里,隐私合规就不再是提审前的一次“临时补作业”,而会变成应用质量的一部分。
对用户来说,这意味着他知道自己的数据去了哪里;对开发者来说,这也意味着权限逻辑更清楚、异常分支更完整、SDK 接入更可控,应用反而会更稳定。
最后再提醒一次:HarmonyOS API、应用市场要求和相关规则会持续更新。本文提供的是工程落地思路,正式开发和提交审核时,请结合应用面向的地区、业务类型、目标 API 版本,并以 AppGallery Connect 展示的最新要求及 HarmonyOS 最新开发文档为准。
如果这篇文章帮你理清了权限、隐私政策和数据处理之间的关系,欢迎点赞、收藏。你也可以把遇到的审核意见留在评论区,我们一起把问题翻译成能落地的代码。
更多推荐
所有评论(0)