在这里插入图片描述

📖 引言

经过前面两篇的学习,我们的「民族图鉴」已经能在多种设备上运行,也支持多语言了。但还有一件非常重要的事情没做——安全加固

你可能会问:

  • 「民族图鉴」就是一个文化类应用,没什么敏感数据,需要做安全吗?
  • 什么是代码混淆?混淆了还能调试吗?
  • 数据安全怎么做?哪些数据需要加密?
  • 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。

不能混淆的情况

  1. 对外的接口

    • 如果你有对外暴露的 API(比如给其他 App 调用)
    • 接口名不能混,否则别人调不到
  2. 和原生交互的部分

    • 调用 Native 模块的方法名
    • 反射调用的类名、方法名
    • 这些混了,就找不到了
  3. JSON 序列化的字段名

    • 如果你把对象序列化成 JSON,发给后端
    • 字段名混了,后端就解析不了了
  4. 资源引用

    • $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);
  }
}

💡 密钥管理的原则

  1. 密钥不要硬编码在代码里
  2. 密钥不要和加密数据存在同一个地方
  3. 尽量用系统提供的安全能力(HUKS、TEE)
  4. 密钥要能轮换(定期更换,泄露了能换)

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;
  }
  
  // 其他方法类似...
}

💡 加密存储的原则

  1. 只加密敏感数据,不加密所有数据(性能考虑)
  2. 加密和存储分开,调用方不用关心细节
  3. 密钥存在安全的地方(HUKS / TEE),不要硬编码
  4. 加密失败要有降级策略(比如清除数据、提示用户)

步骤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)不能硬编码在代码里!混淆了也不行,认真找总能找到。更安全的方式是:

  1. 密钥存在安全区(HUKS/TEE)
  2. 用 native 层存,增加反编译难度
  3. 动态获取,每次启动从服务器取(但首次启动怎么办?)
  4. 结合设备指纹,每个设备的密钥不一样

没有绝对安全的方式,只是提高攻击成本。


步骤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 写单元测试,验证加解密正确性

排查方法

  1. 先关掉所有加固和混淆,看看是不是本身就有 bug
  2. 逐步开启加固选项,定位是哪一步出的问题
  3. 用 namecache 文件,把混淆后的堆栈还原回来
  4. 保留符号表,方便调试

💡 预防方法

  • 混淆规则要写全,提前把不能混的都列出来
  • 加固后一定要做完整的回归测试
  • 不要只在一个机型上测,多测几个机型和系统版本

问题2:混淆后调试困难,怎么排查问题?

现象:线上出了 bug,堆栈里都是 a、b、c 这样的混淆名,看不懂。

解决方法

  1. 保留映射文件

    • 混淆的时候生成 namecache 文件
    • 用这个文件可以把混淆后的名字还原回来
    • 每个版本都要保存好对应的映射文件
  2. Debug 版本不混淆

    • 开发和测试用 Debug 包,不混淆
    • 只有 Release 包才混淆
    • 这样开发调试不受影响
  3. 关键位置打日志

    • 重要的地方打日志,用 TAG 标记
    • 出问题了看日志,不用看堆栈
  4. 线上监控

    • 接入崩溃监控平台
    • 上传符号表,自动还原堆栈

问题3:加密了会不会很卡?影响性能吗?

答案:对普通应用来说,影响可以忽略不计。

为什么?

  • 加密解密的速度很快,尤其是 AES(现代 CPU 有硬件加速)
  • 我们只加密敏感数据,不是所有数据
  • 敏感数据量一般不大(几 KB 到几 MB)

什么时候会有性能问题?

  • 加密大文件(几百 MB 以上)
  • 频繁加密解密(每秒几十上百次)
  • 用了很慢的加密算法(比如 RSA 加密大数据)

优化建议

  • 大数据用对称加密(AES),速度快
  • 非对称加密只用来加密密钥,不用来加密数据
  • 加密后的数据缓存起来,不要每次都解密
  • 异步操作,不要在主线程加密大数据

问题4:隐私合规审核不通过,常见原因有哪些?

现象:上架应用市场,隐私合规审核被拒了。

常见原因

  1. 没有隐私政策

    • 或者隐私政策藏得太深,找不到
    • 解决:设置里加明显的入口,启动时弹窗展示
  2. 过度申请权限

    • 申请了用不到的权限
    • 解决:删掉不必要的权限,只留必须的
  3. 未告知就收集信息

    • 用户还没同意隐私政策,就开始收集数据
    • 解决:用户同意后再初始化统计、推送等 SDK
  4. 没有用户数据删除入口

    • 用户想删自己的数据,找不到地方删
    • 解决:设置里加"清除数据"、"注销账号"功能
  5. 权限申请没有说明原因

    • 弹权限申请框,但不说为什么要这个权限
    • 解决:申请权限前先弹个说明,告诉用户为什么要

💡 合规建议
上架前,认真读一遍应用市场的审核规范,一条一条对照。很多审核被拒都是低级错误,仔细点就能避免。


问题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 测试的流程与方法
  • 📈 了解用户增长模型:获客、激活、留存、转化
  • 🚀 实战实现「民族图鉴」埋点系统
  • 🚩 分析数据分析的常见问题与解决方案

🔗 相关链接


💡 结语:安全是应用的底线。没有安全,再好用的功能、再漂亮的界面都是空中楼阁。安全不是某一个人的事,而是每个开发者的责任。写代码的时候多想一步"这样写安全吗?",用户的数据就多一份保障。下一篇,我们来聊数据分析与运营——让数据驱动产品迭代。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐