HarmonyOS应用《民族图鉴》开发第98篇:安全加固——代码混淆/安全检测/隐私合规

📖 引言
经过前面两篇的学习,我们的「民族图鉴」已经能在多种设备上运行,也支持多语言了。但还有一件非常重要的事情没做——安全加固。
你可能会问:
- 「民族图鉴」就是一个文化类应用,没什么敏感数据,需要做安全吗?
- 什么是代码混淆?混淆了还能调试吗?
- 数据安全怎么做?哪些数据需要加密?
- HTTPS 就够安全了吗?还需要做什么?
- 隐私合规是什么?要注意哪些法律法规?
- 加固了会不会影响性能?会不会崩?
这些问题非常关键。很多开发者对安全的态度是"我又不是做金融的,安全跟我没关系"。但实际上,安全是每个应用都应该重视的事情。数据泄露、应用被破解、隐私合规问题——这些事情发生了,后果可能很严重。
本文将从为什么需要安全讲起,系统讲解应用安全的四个层次:代码安全、数据安全、网络安全、隐私合规。并以「民族图鉴」为例,给出一套完整的安全加固方案。读完本文,你将建立起全面的安全意识,掌握实用的安全加固技巧。
🎯 学习目标
完成本文后,你将能够:
- ✅ 理解应用安全的重要性与四个层次
- ✅ 掌握代码混淆与加固的方法
- ✅ 学会数据加密存储与密钥管理
- ✅ 理解网络安全:HTTPS、证书校验、防抓包
- ✅ 掌握隐私合规的核心原则与实践
- ✅ 了解安全检测的方法与工具
- ✅ 实战实现「民族图鉴」安全加固方案
- ✅ 了解安全加固的常见问题与解决方案
💡 需求分析
为什么需要安全?
在动手之前,我们先想清楚:「民族图鉴」这样一个文化类应用,需要做安全吗?答案是肯定的。
1. 数据泄露的风险
不要以为文化类应用就没有敏感数据。想想看:
- 用户个人信息:昵称、头像、收藏记录、浏览历史
- 认证凭证:如果有账号系统,Token、密码这些
- 支付信息:如果有付费功能
- 行为数据:用户的浏览习惯、兴趣偏好
这些数据如果泄露了,用户的隐私就受到了侵犯。哪怕只是收藏和浏览历史,也能反映出用户的兴趣和身份。
2. 应用被破解的风险
应用如果不加保护,很容易被破解:
- 逆向工程:反编译出源码,窃取逻辑
- 盗版:改个名字、加个广告,重新打包上架
- 篡改:修改数据、破解付费功能
- 注入恶意代码:植入病毒、窃取信息
你辛苦做的应用,别人花几天就破解了,换个皮就拿去赚钱,甚至干坏事——你说冤不冤?
3. 恶意攻击的风险
只要你的应用连网,就可能受到攻击:
- 网络抓包:窃取传输中的数据
- 中间人攻击:篡改网络请求和响应
- 重放攻击:截获请求,重复发送
- 注入攻击:SQL 注入、XSS 等
4. 合规要求
现在隐私合规越来越严了:
- 个人信息保护法(PIPL):中国的隐私法,2021年实施
- 网络安全法:网络运营者的安全义务
- 数据安全法:数据分类分级、重要数据保护
- 应用商店审核:上架应用市场需要通过安全审核
不合规的话,轻则被下架,重则罚款。
💡 安全不是"有没有"的问题,而是"程度"的问题。银行级的应用需要最高级别的安全,文化类应用可以适当降低要求,但基本的安全还是要做的。就像你家大门——你不用装银行金库的门,但至少得有把锁吧?
应用安全的四个层次
应用安全不是单一的概念,而是多层次的防御体系。就像一个城堡,有城墙、有城门、有卫兵、有暗道——层层防御。
应用安全的四个层次
┌─────────────────────────────────┐
│ 第四层:隐私合规 │
│ 权限最小化 · 用户授权 · 透明 │
└─────────────┬───────────────────┘
│
┌─────────────▼───────────────────┐
│ 第三层:网络安全 │
│ HTTPS · 证书校验 · 接口签名 │
└─────────────┬───────────────────┘
│
┌─────────────▼───────────────────┐
│ 第二层:数据安全 │
│ 加密存储 · 加密算法 · 密钥管理 │
└─────────────┬───────────────────┘
│
┌─────────────▼───────────────────┐
│ 第一层:代码安全 │
│ 混淆 · 加固 · 反调试 · 防篡改 │
└─────────────────────────────────┘
- 第一层:代码安全 —— 保护你的代码不被轻易破解和篡改
- 第二层:数据安全 —— 保护用户的数据不被窃取和篡改
- 第三层:网络安全 —— 保护数据传输过程中的安全
- 第四层:隐私合规 —— 确保收集和使用数据符合法律法规
这四层是层层递进、互相支撑的。每一层都很重要,不能偏废。
「民族图鉴」安全需求分析
「民族图鉴」是一个文化类应用,安全需求和金融类应用不一样。我们来分析一下:
风险等级评估
| 风险项 | 风险等级 | 说明 |
|---|---|---|
| 代码被反编译 | 中 | 核心逻辑被抄走,盗版风险 |
| 用户数据泄露 | 中高 | 收藏、历史、设置等隐私数据 |
| 网络数据被窃听 | 低 | 主要是公开的民族数据,不敏感 |
| 账号被盗 | 低 | 目前没有账号系统 |
| 支付欺诈 | 无 | 没有支付功能 |
| 合规风险 | 中 | 隐私政策、权限申请需要合规 |
安全策略优先级
P0(必须做):
├── 代码混淆(防止轻易反编译)
├── 敏感数据加密存储(用户Token、密码等)
├── HTTPS 传输(基本要求)
└── 隐私合规(权限最小化、隐私政策)
P1(应该做):
├── 防调试、防注入
├── 接口签名(防止篡改请求)
├── 证书校验(防抓包)
└── 静态安全扫描
P2(可以后面做):
├── 加壳加固
├── 渗透测试
├── 数据防泄漏(DLP)
└── 安全运营中心(SOC)
💡 安全的度:安全不是越严越好。安全和用户体验、开发效率、性能,都是有取舍的。安全做得太严,可能用户用起来麻烦,开发效率低,性能也受影响。要根据应用的风险等级,找到一个平衡点。
🛠️ 核心实现
步骤1:代码安全——混淆与加固
代码安全是最基础的一层。如果代码能被轻易反编译,那其他层的安全也都是空中楼阁——攻击者看了你的源码,什么加密算法、什么密钥,都一目了然。
1.1 为什么需要代码混淆?
ArkTS/JavaScript 是解释型语言,天生容易被反编译。虽然鸿蒙有编译优化,但如果不做混淆,反编译出来的代码可读性还是很高。
没有混淆的代码:
function getUserProfile(userId) {
const user = database.queryUser(userId);
const profile = calculateUserLevel(user);
return { name: user.name, level: profile.level };
}
混淆后的代码:
function a(b) {
const c = d.e(b);
const f = g(c);
return { h: c.h, i: f.i };
}
混淆之后,变量名、函数名变成了无意义的字母,逻辑还是那个逻辑,但人很难看懂。攻击者要分析代码,就得花更多时间。
⚠️ 注意:混淆不是加密,只是增加阅读难度。理论上,只要花足够时间,混淆后的代码还是能看懂的。但安全的本质就是"提高攻击成本"——让攻击成本大于攻击收益,攻击者就懒得攻了。
1.2 鸿蒙代码混淆配置
鸿蒙的代码混淆是在 obfuscation-rules.txt 文件中配置的。项目里已经有了这个文件,我们来详细讲解一下:
# obfuscation-rules.txt
# ========== 混淆开关 ==========
# 禁用所有混淆(调试的时候可以打开)
# -disable-obfuscation
# 启用属性名混淆(把对象的属性名改成短名字)
-enable-property-obfuscation
# 启用顶层名字混淆(全局变量、函数名)
-enable-toplevel-obfuscation
# 启用文件名混淆
-enable-filename-obfuscation
# 启用导出混淆(导出的函数、类名也混淆)
-enable-export-obfuscation
# 压缩:删除不必要的空格和换行
-compact
# 移除日志:删除所有 console.* 语句
-remove-log
# 输出名称映射文件(方便调试的时候还原)
-print-namecache
# 使用缓存的名称映射(保持不同版本的混淆一致)
# -apply-namecache
# ========== 保留规则 ==========
# 保留指定的属性名(不能混淆,否则外部调用会出错)
# -keep-property-name <属性名>
# 保留指定的全局名字
# -keep-global-name <名字>
1.3 混淆的注意事项
混淆不是一股脑全混了就行,有些东西不能混,混了就出 bug。
不能混淆的情况:
-
对外的接口
- 如果你有对外暴露的 API(比如给其他 App 调用)
- 接口名不能混,否则别人调不到
-
和原生交互的部分
- 调用 Native 模块的方法名
- 反射调用的类名、方法名
- 这些混了,就找不到了
-
JSON 序列化的字段名
- 如果你把对象序列化成 JSON,发给后端
- 字段名混了,后端就解析不了了
-
资源引用
$r('app.string.xxx')里的资源名- 这些是资源系统的,代码混淆影响不到,但要注意
配置示例:
# 保留 API 接口的属性名
-keep-property-name userId
-keep-property-name userName
-keep-property-name token
# 保留某个类的所有属性
# (实际使用时按需要加)
1.4 反调试与防篡改
混淆只是第一步,更高级的保护还有反调试、防篡改。
反调试:检测应用是否被调试器附加,如果是就退出或瘫痪。
// 简化的反调试检测(示意,实际项目中要更复杂)
export class AntiDebug {
// 检测是否被调试
static isDebuggerAttached(): boolean {
// 方法1:检测调试端口
// 方法2:检测时间差(断点会导致执行时间变长)
// 方法3:检测 Debug.isDebuggerConnected()
return false; // 简化
}
// 启动反调试保护
static startProtection(): void {
// 定期检测
setInterval(() => {
if (this.isDebuggerAttached()) {
// 检测到调试,采取措施
// - 退出应用
// - 清除敏感数据
// - 让应用瘫痪(但不要太明显)
console.error('Debugger detected!');
}
}, 3000);
}
}
防篡改:检测应用是否被篡改过(比如重新打包、注入代码)。
// 防篡改检测(示意)
export class AntiTamper {
// 校验签名
static verifySignature(): boolean {
// 获取应用签名
// 和预期的签名对比
// 不一致说明被篡改了
return true; // 简化
}
// 校验完整性
static verifyIntegrity(): boolean {
// 计算关键文件的哈希
// 和预期的哈希对比
return true; // 简化
}
}
💡 矛与盾的关系:没有绝对安全的保护。只要攻击者足够有耐心和能力,理论上任何保护都能被攻破。但安全的意义在于"提高攻击成本"——让破解你的应用,比从零开发一个还贵,那就够了。
1.5 加壳与加固
除了代码混淆,还有更高级的保护方式——加壳和加固。
什么是加壳?
加壳就是给你的应用包一层"壳",运行的时候先运行壳程序,壳程序解密真正的代码,然后再执行。这样直接反编译出来的只是壳的代码,看不到真正的业务逻辑。
鸿蒙的加固方案:
华为提供了官方的应用加固服务(AppGallery Connect 应用加固),支持鸿蒙应用。
加固的层级:
1. 代码混淆(基础保护,免费,自己就能做)
2. 资源混淆(保护图片、布局等资源)
3. 加壳(DEX/ABC 加壳,更强的保护)
4. 虚拟化(把关键代码变成虚拟机指令,最强保护)
「民族图鉴」的选择:
- 第一阶段:先用代码混淆(成本低,见效快)
- 第二阶段:上架后,考虑用官方加固服务
- 不需要太高级的加固(毕竟不是金融类应用)
步骤2:数据安全——加密存储
第二层是数据安全。应用在本地存了很多数据,哪些需要加密?怎么加密?密钥怎么管?
2.1 哪些数据需要加密?
不是所有数据都需要加密。加密有性能开销,也要花开发成本。我们只加密敏感数据。
| 数据类型 | 敏感度 | 是否需要加密 | 说明 |
|---|---|---|---|
| 密码、Token | 高 | ✅ 必须 | 泄露了账号就丢了 |
| 身份证号、手机号 | 高 | ✅ 必须 | 个人敏感信息(PII) |
| 聊天记录、私信 | 高 | ✅ 必须 | 用户隐私 |
| 收藏记录、浏览历史 | 中 | ⚠️ 建议 | 能反映用户偏好 |
| 用户昵称、头像 | 低 | ❌ 不用 | 公开信息 |
| 应用设置、主题 | 低 | ❌ 不用 | 不敏感 |
| 缓存的公开数据 | 低 | ❌ 不用 | 民族介绍、图片等 |
「民族图鉴」目前没有账号系统,也没有手机号、身份证号这些。但如果以后加了账号,Token 和密码肯定要加密存。
目前需要加密的:
- 用户的收藏列表(隐私数据)
- 浏览历史(能反应用户兴趣)
2.2 加密算法选择
加密算法有很多种,选哪种?
两大类加密算法:
对称加密(Symmetric):
加密和解密用同一个密钥
特点:速度快,适合大数据量
常见算法:AES、DES、SM4
非对称加密(Asymmetric):
加密和解密用不同的密钥(公钥加密,私钥解密)
特点:安全,但速度慢
常见算法:RSA、ECC、SM2
选择原则:
- 本地存储加密 → 用对称加密(AES),速度快
- 网络传输密钥 → 用非对称加密(RSA),安全
- 国密要求 → 用 SM 系列(SM2/SM3/SM4)
「民族图鉴」的选择:
- 本地数据加密:AES-256-GCM
- AES 是最主流的对称加密算法
- 256 位密钥,安全强度高
- GCM 模式,既能加密又能校验完整性
- 如果有国密要求,换成 SM4-GCM
2.3 密钥管理
加密的密钥怎么存?这是个大问题。你把密钥存在代码里,别人反编译就能拿到;存在文件里,别人能读文件。
密钥管理的层级:
密钥管理金字塔
┌─────────────────────────┐
│ 主密钥(Master Key) │ ← 存在安全硬件里
│ (最顶层,保护其他密钥)│ (TEE / 安全芯片)
├─────────────────────────┤
│ 密钥加密密钥(KEK) │ ← 加密数据加密密钥
│ (加密 DEK 的密钥) │
├─────────────────────────┤
│ 数据加密密钥(DEK) │ ← 加密用户数据
│ (真正加密数据的密钥) │
└─────────────────────────┘
- DEK(Data Encryption Key):数据加密密钥,真正用来加密用户数据的。可以每个用户一个,甚至每个文件一个。
- KEK(Key Encryption Key):密钥加密密钥,用来加密 DEK 的。DEK 加密后存下来,用 KEK 解密。
- Master Key:主密钥,最顶层的密钥,用来加密 KEK。存在最安全的地方。
鸿蒙的安全能力:
鸿蒙提供了 HUKS(HarmonyOS Universal KeyStore) 统一密钥管理服务,还有 TEE(可信执行环境)。密钥可以存在安全区里,外部拿不到。
// 使用 HUKS 管理密钥(简化示例)
import { huks } from '@kit.CryptoArchitectureKit';
class KeyManager {
// 生成并存储密钥
async generateKey(alias: string): Promise<void> {
// 生成 AES 密钥,存在 HUKS 里
const keyProperties = {
keyAlias: alias,
keyType: huks.KeyType.HUKS_KEY_TYPE_AES,
keyLen: 256,
purpose: huks.KeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT |
huks.KeyPurpose.HUKS_KEY_PURPOSE_DECRYPT,
padding: huks.KeyPadding.HUKS_PADDING_GCM,
mode: huks.KeyMode.HUKS_MODE_GCM,
// ... 其他参数
};
// 生成密钥
await huks.generateKey(keyProperties);
}
// 加密数据
async encrypt(alias: string, plainData: Uint8Array): Promise<Uint8Array> {
// 用 HUKS 里的密钥加密
// 密钥不会离开安全区
// ...
return new Uint8Array(0);
}
// 解密数据
async decrypt(alias: string, cipherData: Uint8Array): Promise<Uint8Array> {
// ...
return new Uint8Array(0);
}
}
💡 密钥管理的原则:
- 密钥不要硬编码在代码里
- 密钥不要和加密数据存在同一个地方
- 尽量用系统提供的安全能力(HUKS、TEE)
- 密钥要能轮换(定期更换,泄露了能换)
2.4 「民族图鉴」加密存储实战
我们来给「民族图鉴」加一个加密存储的功能,加密保存用户的敏感数据。
首先,封装一个加密工具:
// common/utils/CryptoUtils.ets
import { huks } from '@kit.CryptoArchitectureKit';
/**
* 加密工具类
* 使用 AES-256-GCM 加密,密钥存在 HUKS 中
*/
export class CryptoUtils {
private static readonly KEY_ALIAS = 'ethnic_chronicles_storage_key';
private static isKeyReady: boolean = false;
// 初始化密钥
static async init(): Promise<void> {
// 检查密钥是否存在
const isKeyExist = await this.isKeyExist();
if (!isKeyExist) {
// 不存在就生成一个
await this.generateKey();
}
this.isKeyReady = true;
}
// 检查密钥是否存在
private static async isKeyExist(): Promise<boolean> {
try {
const keyProperties = {
keyAlias: this.KEY_ALIAS,
keyType: huks.KeyType.HUKS_KEY_TYPE_AES,
};
// 查询密钥是否存在
await huks.getKeyProperties(keyProperties);
return true;
} catch (e) {
return false;
}
}
// 生成密钥
private static async generateKey(): Promise<void> {
const keyProperties = {
keyAlias: this.KEY_ALIAS,
keyType: huks.KeyType.HUKS_KEY_TYPE_AES,
keyLen: 256,
purpose: huks.KeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT |
huks.KeyPurpose.HUKS_KEY_PURPOSE_DECRYPT,
padding: huks.KeyPadding.HUKS_PADDING_GCM,
mode: huks.KeyMode.HUKS_MODE_GCM,
digest: huks.KeyDigest.HUKS_DIGEST_NONE,
};
await huks.generateKey(keyProperties);
}
// 加密字符串
static async encryptString(plainText: string): Promise<string> {
if (!this.isKeyReady) {
await this.init();
}
// 把字符串转成 Uint8Array
const plainData = new TextEncoder().encode(plainText);
// 生成随机 IV(GCM 模式需要)
const iv = crypto.getRandomValues(new Uint8Array(12));
// 加密
const cipherData = await this.encrypt(plainData, iv);
// 把 IV 和密文拼在一起,Base64 编码
// IV 不需要保密,但要和密文一起存
const result = new Uint8Array(iv.length + cipherData.length);
result.set(iv, 0);
result.set(cipherData, iv.length);
return this.uint8ToBase64(result);
}
// 解密字符串
static async decryptString(cipherText: string): Promise<string> {
if (!this.isKeyReady) {
await this.init();
}
// Base64 解码
const data = this.base64ToUint8(cipherText);
// 拆开 IV 和密文
const iv = data.slice(0, 12);
const cipherData = data.slice(12);
// 解密
const plainData = await this.decrypt(cipherData, iv);
// 转成字符串
return new TextDecoder().decode(plainData);
}
// 加密(底层调用 HUKS)
private static async encrypt(
plainData: Uint8Array,
iv: Uint8Array
): Promise<Uint8Array> {
// 实际项目中调用 HUKS API
// 这里简化处理
// ...
return new Uint8Array(0);
}
// 解密(底层调用 HUKS)
private static async decrypt(
cipherData: Uint8Array,
iv: Uint8Array
): Promise<Uint8Array> {
// 实际项目中调用 HUKS API
// 这里简化处理
// ...
return new Uint8Array(0);
}
// Uint8Array 转 Base64
private static uint8ToBase64(data: Uint8Array): string {
// 转换逻辑...
return '';
}
// Base64 转 Uint8Array
private static base64ToUint8(base64: string): Uint8Array {
// 转换逻辑...
return new Uint8Array(0);
}
}
然后,改造 StorageService,让它支持加密存储:
// services/StorageService.ets (改造版)
import { Preferences } from '@kit.ArkData';
import { CryptoUtils } from '../common/utils/CryptoUtils';
export class StorageService {
private static instance: StorageService;
private preferences: Preferences | null = null;
// 哪些 key 需要加密存储
private static readonly ENCRYPTED_KEYS = new Set([
'user_token',
'user_password',
'favorite_list',
'browse_history'
]);
private constructor() {}
static getInstance(): StorageService {
if (!StorageService.instance) {
StorageService.instance = new StorageService();
}
return StorageService.instance;
}
async init(context: Context): Promise<void> {
// 初始化 Preferences...
// 初始化加密工具
await CryptoUtils.init();
}
// 保存字符串(自动判断是否需要加密)
async saveString(key: string, value: string): Promise<void> {
if (!this.preferences) return;
let storedValue = value;
// 如果是敏感数据,先加密
if (StorageService.ENCRYPTED_KEYS.has(key)) {
storedValue = await CryptoUtils.encryptString(value);
}
await this.preferences.put(key, storedValue);
await this.preferences.flush();
}
// 获取字符串(自动判断是否需要解密)
async getString(key: string, defaultValue: string = ''): Promise<string> {
if (!this.preferences) return defaultValue;
const storedValue = await this.preferences.get(key, defaultValue);
if (storedValue === defaultValue) return defaultValue;
// 如果是敏感数据,解密后返回
if (StorageService.ENCRYPTED_KEYS.has(key)) {
try {
return await CryptoUtils.decryptString(storedValue as string);
} catch (e) {
console.error('Decrypt failed', e);
return defaultValue;
}
}
return storedValue as string;
}
// 其他方法类似...
}
💡 加密存储的原则:
- 只加密敏感数据,不加密所有数据(性能考虑)
- 加密和存储分开,调用方不用关心细节
- 密钥存在安全的地方(HUKS / TEE),不要硬编码
- 加密失败要有降级策略(比如清除数据、提示用户)
步骤3:网络安全
第三层是网络安全。数据在网络上传输,会不会被窃听?会不会被篡改?
3.1 HTTPS 是基础
最基本的网络安全就是 HTTPS。HTTPS 在 HTTP 的基础上加了一层 SSL/TLS 加密,传输的数据都是加密的。
HTTP vs HTTPS:
HTTP:明文传输
客户端 ────────明文───────→ 服务器
中间人能看到所有内容,还能篡改
HTTPS:加密传输
客户端 ────────密文───────→ 服务器
中间人只能看到密文,看不懂也改不了
为什么一定要用 HTTPS?
- 防止数据被窃听(密码、Token 等)
- 防止数据被篡改(中间人修改请求/响应)
- 防止钓鱼(验证服务器身份)
- 应用商店审核要求
现在几乎所有应用都用 HTTPS 了。「民族图鉴」的 API 服务也一定要用 HTTPS。
3.2 证书校验(防抓包)
只用 HTTPS 还不够。因为攻击者可以安装自己的证书,用代理工具抓包(比如 Charles、Fiddler)。
普通 HTTPS:
客户端 ────HTTPS──── 代理 ────HTTPS──── 服务器
(中间人) (中间人)
攻击者只要让用户信任了代理的证书,就能解密所有流量
证书钉扎(Certificate Pinning):
客户端内置服务器证书或公钥
只信任这个证书,其他证书都不认
这样代理的证书就没用了
证书钉扎(Certificate Pinning):
把服务器的证书(或公钥哈希)内置在客户端里。客户端只信任这个证书,别的证书就算是系统信任的也不认。这样中间人攻击就失效了。
鸿蒙中怎么做证书校验?
// HttpClient 证书校验(简化示例)
import { http } from '@kit.NetworkKit';
class SecureHttpClient {
async request(url: string, options: any): Promise<any> {
// 配置证书钉扎
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(url, {
method: options.method,
header: options.headers,
body: options.body,
// 证书配置
caData: this.getServerCertificate(), // 服务器证书
// ...
});
return response;
} catch (e) {
console.error('Request failed', e);
throw e;
}
}
// 获取服务器证书(内置的)
private getServerCertificate(): string {
// 从资源文件读取证书
// 或者用公钥哈希
return '';
}
}
⚠️ 证书钉扎的坑:证书钉扎虽然安全,但如果服务器证书换了,旧版本的 App 就连接不上了。所以要预留更新机制,或者用证书链钉扎(钉住根证书或中间证书,而不是叶子证书)。
3.3 接口签名(防篡改)
HTTPS 防的是传输过程中被窃听和篡改。但如果攻击者直接调用你的接口呢?比如构造一个假的请求,刷你的服务器。
这时候需要接口签名。
什么是接口签名?
客户端把请求参数 + 密钥,按一定规则算出一个签名(sign),和请求一起发给服务器。服务器用同样的规则算一遍,如果签名一致,说明请求是合法的。
接口签名流程:
客户端:
1. 把所有参数按字典序排序
2. 拼成字符串:key1=value1&key2=value2...
3. 加上密钥:...&secret=xxx
4. 计算 MD5/SHA256,得到 sign
5. 把 sign 加到请求里
服务器:
1. 收到请求,取出所有参数
2. 用同样的规则算 sign
3. 和收到的 sign 对比
4. 一致 → 合法;不一致 → 拒绝
接口签名的作用:
- 防止参数被篡改(改了任何参数,签名就不对了)
- 防止伪造请求(不知道密钥,造不出正确的签名)
- 配合时间戳,还能防重放攻击
防重放攻击:
攻击者截获一个合法请求,然后重复发送很多次,刷你的服务器。这就是重放攻击。
怎么防?加个时间戳:
- 请求里带上当前时间戳 timestamp
- 服务器收到后,检查时间戳和服务器时间差多少
- 超过 5 分钟(可配置)就拒绝
- 再用一个 nonce(随机数),保证同一个请求只能用一次
// 接口签名工具(简化示例)
export class ApiSigner {
private static readonly API_SECRET = 'your_api_secret_here'; // 不要硬编码!
private static readonly TIMESTAMP_TOLERANCE = 5 * 60 * 1000; // 5 分钟容差
// 生成签名
static sign(params: Record<string, any>): string {
// 1. 加上时间戳
params.timestamp = Date.now().toString();
// 2. 加上随机数
params.nonce = this.generateNonce();
// 3. 参数按 key 排序
const sortedKeys = Object.keys(params).sort();
// 4. 拼接成字符串
let signStr = '';
for (const key of sortedKeys) {
if (signStr) signStr += '&';
signStr += `${key}=${params[key]}`;
}
// 5. 加上密钥
signStr += `&secret=${this.API_SECRET}`;
// 6. 计算 SHA256
return this.sha256(signStr);
}
// 验证签名(服务器端做,客户端不用)
static verify(params: Record<string, any>, sign: string): boolean {
// 1. 检查时间戳
const timestamp = parseInt(params.timestamp);
if (Math.abs(Date.now() - timestamp) > this.TIMESTAMP_TOLERANCE) {
return false; // 请求过期
}
// 2. 重新算签名,对比
const calculatedSign = this.sign(params);
return calculatedSign === sign;
}
private static generateNonce(): string {
return Math.random().toString(36).substring(2, 10);
}
private static sha256(str: string): string {
// 计算 SHA-256
return ''; // 简化
}
}
⚠️ 密钥安全:接口签名的密钥(secret)不能硬编码在代码里!混淆了也不行,认真找总能找到。更安全的方式是:
- 密钥存在安全区(HUKS/TEE)
- 用 native 层存,增加反编译难度
- 动态获取,每次启动从服务器取(但首次启动怎么办?)
- 结合设备指纹,每个设备的密钥不一样
没有绝对安全的方式,只是提高攻击成本。
步骤4:隐私合规
第四层是隐私合规。现在隐私保护越来越受重视,法律法规也越来越严。不合规的应用,轻则被下架,重则罚款。
4.1 重要的法律法规
和我们最相关的几部法律:
| 法律 | 简称 | 主要内容 |
|---|---|---|
| 《中华人民共和国个人信息保护法》 | PIPL | 个人信息的收集、使用、存储、传输规则 |
| 《中华人民共和国网络安全法》 | 网安法 | 网络运营者的安全义务 |
| 《中华人民共和国数据安全法》 | 数安法 | 数据分类分级、重要数据保护 |
| 《通用数据保护条例》 | GDPR | 欧盟的隐私法(如果做海外) |
| 《加州消费者隐私法案》 | CCPA | 美国加州的隐私法 |
我们重点看一下 PIPL,因为这是国内最相关的。
4.2 PIPL 核心原则
PIPL 的核心原则有几个:
1. 合法、正当、必要、诚信原则
- 不能骗用户,不能搞小动作
- 收集信息要光明正大
2. 最小必要原则
- 只收集必要的信息
- 能少收集就少收集
- 能用低敏感度的就不用高敏感度的
- 比如:能要昵称就别要真名,能要手机号就别要身份证
3. 知情同意原则
- 用户得知道你收集了什么、用来干嘛
- 用户得同意了才能收集
- 不同意就不能强制(除非是必要功能)
4. 目的限定原则
- 收集的时候说好了用来干嘛,就只能用来干嘛
- 不能挪作他用
- 比如:收集手机号是为了登录,就不能拿去营销(除非用户另行同意)
5. 公开透明原则
- 隐私政策要写清楚
- 不能藏在犄角旮旯
- 用户得能 easily 找到
6. 质量保障原则
- 数据要准确
- 数据要及时更新
- 用户能更正自己的信息
7. 责任与安全原则
- 数据收集者对数据安全负责
- 出了问题要承担责任
4.3 「民族图鉴」合规要点
「民族图鉴」是个文化类应用,数据收集不多,但也要注意合规。
权限申请:
- 只申请必要的权限:需要什么申请什么,不要一上来就申请一堆
- 申请时说明原因:为什么要这个权限,用来干嘛
- 用户拒绝了也要能用:不要因为用户不给权限就不让用 App(核心功能除外)
「民族图鉴」的权限分析:
| 权限 | 是否需要 | 原因 | 合规要点 |
|---|---|---|---|
| 网络权限 | ✅ 需要 | 加载数据、AI 对话 | 基础权限,默认申请 |
| 存储权限 | ✅ 需要 | 缓存图片、数据 | 只访问应用沙箱,不用申请外部存储 |
| 相机权限 | ⚠️ 可选 | 头像、拍照上传 | 用到的时候再申请,说明原因 |
| 麦克风权限 | ⚠️ 可选 | 语音输入、TTS? | TTS 不需要麦克风,语音输入才需要 |
| 定位权限 | ❌ 不需要 | 目前没有定位相关功能 | 不要申请 |
| 通讯录权限 | ❌ 不需要 | 完全用不到 | 不要申请 |
| 短信权限 | ❌ 不需要 | 完全用不到 | 不要申请 |
💡 权限最小化原则:
- 能用低权限的就不用高权限
- 能不用权限实现的就不用权限
- 用到的时候再申请(运行时申请),不要一启动就申请一堆
- 用户拒绝了,要有降级方案,不要不让用 App
4.4 隐私政策
隐私政策是合规的必备项。上架应用市场必须要有隐私政策。
隐私政策要包含什么?
隐私政策的基本框架:
1. 我们是谁(运营主体)
2. 我们收集什么信息
3. 我们怎么使用这些信息
4. 我们怎么存储和保护这些信息
5. 我们会分享这些信息吗(给谁、为什么)
6. 你的权利(查看、更正、删除、撤回同意)
7. Cookie 和类似技术
8. 未成年人保护
9. 政策的更新
10. 联系我们
隐私政策的要求:
- 清晰易懂:不要用太多法律术语,普通人要能看懂
- 容易找到:设置里要有入口,启动时可以看到
- 更新通知:政策更新了要通知用户
- 用户同意:用户同意了才能收集数据
4.5 用户权利
用户对自己的个人信息有权利,我们要提供实现这些权利的方式:
| 权利 | 说明 | 怎么实现 |
|---|---|---|
| 知情权 | 知道你收集了什么、用来干嘛 | 隐私政策写清楚 |
| 决定权 | 决定同不同意收集 | 同意弹窗、设置里开关 |
| 查阅权 | 看你收集了他哪些数据 | 个人中心里可以看 |
| 更正权 | 更正不准确的数据 | 个人资料编辑功能 |
| 删除权 | 删除他的个人数据 | 提供删除账号/数据的功能 |
| 撤回同意权 | 撤回之前的同意 | 设置里可以关权限、注销账号 |
| 可携带权 | 把数据带走(导出) | 提供数据导出功能 |
这些权利不是说着玩的,是法律规定的。如果用户要求删除他的数据,你就得删。
步骤5:安全检测
安全加固做了,但做得够不够?有没有漏洞?这就需要安全检测。
5.1 静态安全扫描
静态安全扫描(Static Application Security Testing,SAST)是不运行程序,直接分析代码找漏洞。
常见的静态扫描工具:
- 鸿蒙官方:DevEco Studio 自带安全检测
- 第三方:各种安全扫描工具
- 自己写:写脚本检查常见问题
静态扫描能发现什么?
- 硬编码的密钥、密码
- 不安全的加密算法(DES、MD5 等)
- SQL 注入漏洞
- XSS 漏洞
- 不安全的随机数生成
- 敏感信息泄露(日志里打密码、Token)
- 权限滥用
- …
5.2 动态安全测试
动态安全测试(Dynamic Application Security Testing,DAST)是运行起来测试,模拟真实攻击。
常见的动态测试:
- 渗透测试(Penetration Testing):找安全专家模拟黑客攻击
- 模糊测试(Fuzzing):给程序喂各种奇怪的输入,看会不会崩
- 漏洞扫描:用工具扫常见漏洞
渗透测试的流程:
1. 信息收集
了解应用的功能、技术栈、接口
2. 威胁建模
分析可能的攻击面和攻击路径
3. 漏洞探测
尝试各种攻击手法,找漏洞
4. 漏洞验证
确认漏洞真实存在,不是误报
5. 输出报告
列出发现的漏洞,给出修复建议
5.3 「民族图鉴」安全检测清单
我们列一个清单,看看「民族图鉴」有没有这些常见安全问题:
代码安全:
- 代码是否做了混淆?
- 密钥、密码有没有硬编码?
- 日志里会不会打印敏感信息?
- 有没有反调试、防篡改?
数据安全:
- 敏感数据是否加密存储?
- 加密算法是否安全(AES 而不是 DES)?
- 密钥管理是否安全?
- 退出登录是否清除本地数据?
网络安全:
- 是否使用 HTTPS?
- 是否校验证书?
- 接口是否有签名?
- 是否防重放攻击?
隐私合规:
- 是否有隐私政策?
- 隐私政策是否清晰易懂?
- 是否申请了不必要的权限?
- 用户是否能查看、删除自己的数据?
- 是否有未成年人保护?
- 有没有收集超出必要范围的数据?
💡 安全是持续的过程,不是一次性的工作。不是说这次做了安全加固,以后就不用管了。新功能、新代码,都可能引入新的安全问题。要定期做安全检测,持续改进。
步骤6:「民族图鉴」安全加固方案汇总
讲了这么多,我们来汇总一下「民族图鉴」的安全加固方案。
第一阶段:基础安全(P0,立即做)
1. 代码混淆
- 启用代码混淆、资源混淆
- 配置混淆规则,排除不能混的部分
- 移除日志(-remove-log)
2. HTTPS 传输
- 所有网络请求走 HTTPS
- 不允许 HTTP 请求(配置允许列表)
3. 敏感数据加密
- 收藏列表、浏览历史加密存储
- 如果有账号,Token、密码加密存储
- 用 HUKS 管理密钥
4. 隐私合规
- 完善隐私政策
- 权限最小化,只申请必要的
- 启动时显示隐私政策同意弹窗
- 设置里提供隐私政策入口
第二阶段:增强安全(P1,上线后做)
1. 证书校验
- 内置服务器证书,做证书钉扎
- 防止抓包和中间人攻击
2. 接口签名
- 关键接口加签名
- 防篡改、防重放
3. 反调试、防篡改
- 增加反调试检测
- 增加签名校验,防止被重打包
4. 静态安全扫描
- 定期跑静态扫描
- 修复发现的问题
第三阶段:高级安全(P2,规模大了再做)
1. 应用加固
- 使用官方加固服务
- 加壳、资源保护
2. 渗透测试
- 找第三方安全公司做渗透测试
- 发现深层漏洞
3. 安全运营
- 安全监控,发现异常告警
- 漏洞响应机制
- 定期安全审计
💡 安全投入和收益的平衡:对于「民族图鉴」这样的文化类应用,第一阶段是必须的,花不了多少时间,但能解决 80% 的安全问题。第二阶段建议做,进一步提升安全性。第三阶段就看情况了,如果用户量很大、有付费功能,再考虑。
⚠️ 常见问题与解决方案
问题1:加固后应用崩溃,怎么办?
现象:没加固的时候好好的,加了混淆或加固就崩了。
常见原因:
| 原因 | 说明 | 解决方法 |
|---|---|---|
| 混淆了不该混的 | 反射调用的类名、方法名被混了 | 加 -keep 规则,保留这些名字 |
| 资源名被混了 | 代码里硬编码了资源名,找不到 | 改用 R 类引用,不要用字符串拼 |
| 加壳不兼容 | 某些系统版本或机型不兼容 | 降级加固级别,联系加固服务提供商 |
| 加密解密错了 | 自己写的加密逻辑有 bug | 写单元测试,验证加解密正确性 |
排查方法:
- 先关掉所有加固和混淆,看看是不是本身就有 bug
- 逐步开启加固选项,定位是哪一步出的问题
- 用 namecache 文件,把混淆后的堆栈还原回来
- 保留符号表,方便调试
💡 预防方法:
- 混淆规则要写全,提前把不能混的都列出来
- 加固后一定要做完整的回归测试
- 不要只在一个机型上测,多测几个机型和系统版本
问题2:混淆后调试困难,怎么排查问题?
现象:线上出了 bug,堆栈里都是 a、b、c 这样的混淆名,看不懂。
解决方法:
-
保留映射文件
- 混淆的时候生成 namecache 文件
- 用这个文件可以把混淆后的名字还原回来
- 每个版本都要保存好对应的映射文件
-
Debug 版本不混淆
- 开发和测试用 Debug 包,不混淆
- 只有 Release 包才混淆
- 这样开发调试不受影响
-
关键位置打日志
- 重要的地方打日志,用 TAG 标记
- 出问题了看日志,不用看堆栈
-
线上监控
- 接入崩溃监控平台
- 上传符号表,自动还原堆栈
问题3:加密了会不会很卡?影响性能吗?
答案:对普通应用来说,影响可以忽略不计。
为什么?
- 加密解密的速度很快,尤其是 AES(现代 CPU 有硬件加速)
- 我们只加密敏感数据,不是所有数据
- 敏感数据量一般不大(几 KB 到几 MB)
什么时候会有性能问题?
- 加密大文件(几百 MB 以上)
- 频繁加密解密(每秒几十上百次)
- 用了很慢的加密算法(比如 RSA 加密大数据)
优化建议:
- 大数据用对称加密(AES),速度快
- 非对称加密只用来加密密钥,不用来加密数据
- 加密后的数据缓存起来,不要每次都解密
- 异步操作,不要在主线程加密大数据
问题4:隐私合规审核不通过,常见原因有哪些?
现象:上架应用市场,隐私合规审核被拒了。
常见原因:
-
没有隐私政策
- 或者隐私政策藏得太深,找不到
- 解决:设置里加明显的入口,启动时弹窗展示
-
过度申请权限
- 申请了用不到的权限
- 解决:删掉不必要的权限,只留必须的
-
未告知就收集信息
- 用户还没同意隐私政策,就开始收集数据
- 解决:用户同意后再初始化统计、推送等 SDK
-
没有用户数据删除入口
- 用户想删自己的数据,找不到地方删
- 解决:设置里加"清除数据"、"注销账号"功能
-
权限申请没有说明原因
- 弹权限申请框,但不说为什么要这个权限
- 解决:申请权限前先弹个说明,告诉用户为什么要
💡 合规建议:
上架前,认真读一遍应用市场的审核规范,一条一条对照。很多审核被拒都是低级错误,仔细点就能避免。
问题5:HTTPS 就够安全了吗?还需要做接口签名吗?
答案:看你的安全需求。
HTTPS 解决的问题:
- 传输过程中数据不被窃听
- 传输过程中数据不被篡改
- 验证服务器身份(防钓鱼)
HTTPS 解决不了的问题:
- 客户端被篡改了怎么办(比如破解版 App)
- 用户自己抓包改请求怎么办
- 服务器怎么确认请求是合法客户端发的
什么时候需要接口签名?
- 有支付功能 → 必须有
- 有用户系统 → 建议有
- 接口很容易被刷 → 需要有
- 纯公开数据、只读 → 可以不用
「民族图鉴」目前没有支付、没有账号,主要是公开数据,接口签名可以先不做。等以后有了账号系统、有了写操作,再加。
📝 本章小结
核心知识点
本文系统讲解了应用安全的四个层次,并为「民族图鉴」制定了完整的安全加固方案。
1. 为什么需要安全
- 数据泄露风险:用户隐私、账号信息
- 应用被破解:逆向、盗版、篡改
- 恶意攻击:抓包、注入、重放
- 合规要求:PIPL、网安法、应用市场审核
- 安全是程度问题,不是有和无的问题
2. 应用安全的四个层次
- 第一层:代码安全(混淆、加固、反调试、防篡改)
- 第二层:数据安全(加密存储、加密算法、密钥管理)
- 第三层:网络安全(HTTPS、证书校验、接口签名)
- 第四层:隐私合规(权限最小化、用户授权、透明公开)
3. 代码安全
- 代码混淆:增加阅读难度,提高攻击成本
- 混淆配置:属性混淆、顶层混淆、文件名混淆
- 注意:对外接口、反射调用、JSON 字段不能混
- 反调试:检测调试器,防止动态分析
- 防篡改:校验签名和完整性,防止重打包
- 加壳加固:更强的保护,官方加固服务
4. 数据安全
- 只加密敏感数据,不是所有数据
- 加密算法:AES-256-GCM(对称),RSA(非对称)
- 密钥管理:HUKS/TEE,分层密钥,密钥轮换
- 「民族图鉴」:收藏、历史加密,其他不用
5. 网络安全
- HTTPS 是基础,必须用
- 证书钉扎:防抓包、防中间人攻击
- 接口签名:防篡改、防伪造、防重放
- 时间戳 + nonce:防重放攻击
6. 隐私合规
- PIPL 核心原则:合法正当、最小必要、知情同意、目的限定
- 权限最小化:只申请必要的,用到再申请
- 隐私政策:清晰易懂、容易找到、及时更新
- 用户权利:知情、决定、查阅、更正、删除、撤回
7. 安全检测
- 静态扫描:不运行,直接分析代码找漏洞
- 动态测试:运行起来测,模拟攻击
- 渗透测试:找安全专家模拟黑客
- 安全是持续的过程,不是一次性的
最佳实践总结
✅ 安全从第一天就考虑
不要等功能做完了再加安全
↓
一开始就按安全规范写
↓
后期维护成本低
✅ 最小权限 / 最小收集
能少要权限就少要
能少收集数据就少收集
↓
数据越少,风险越小
合规越容易
✅ 密钥不要硬编码
// ❌ 不好:硬编码密钥
const SECRET_KEY = 'abcdef123456';
// ✅ 好:存在安全区,动态获取
// 用 HUKS / TEE / KeyStore
✅ HTTPS 是标配,不是可选
所有网络请求走 HTTPS
↓
不要留 HTTP 接口
↓
证书校验要做
✅ 混淆不是加密,但很有用
混淆不能 100% 防止破解
↓
但能提高攻击成本
↓
攻击成本 > 攻击收益 → 攻击者就放弃了
✅ 安全是持续改进的过程
没有绝对安全的系统
↓
定期做安全检测
↓
发现问题及时修复
↓
持续改进,越来越好
下一步预告
在下一篇文章中,我们将:
- 📊 了解为什么需要数据分析,以及数据分析的层次
- 🎯 学习指标体系设计:北极星指标、核心指标、过程指标
- 📍 掌握埋点方案设计:页面埋点、点击埋点、曝光埋点
- 📤 了解数据采集与上报策略
- 🧪 学习 A/B 测试的流程与方法
- 📈 了解用户增长模型:获客、激活、留存、转化
- 🚀 实战实现「民族图鉴」埋点系统
- 🚩 分析数据分析的常见问题与解决方案
🔗 相关链接
- 项目源码: GitCode 仓库
- 代码混淆指南: 官方文档
- 应用安全指南: 官方文档
- 数据加密: 官方文档
- 个人信息保护法: 全国人大官网
💡 结语:安全是应用的底线。没有安全,再好用的功能、再漂亮的界面都是空中楼阁。安全不是某一个人的事,而是每个开发者的责任。写代码的时候多想一步"这样写安全吗?",用户的数据就多一份保障。下一篇,我们来聊数据分析与运营——让数据驱动产品迭代。
更多推荐

所有评论(0)