4、安全传输与存储:HTTPS/TLS传输层保护、客户端安全存储方案(Keychain/Keystore)、内存保护与混淆

用户令牌拿到手之后,怎么把它安全地送到服务器?到了客户端又该藏在哪儿?这两个问题要是没想清楚,前面做的认证工作基本白费。

我见过不少团队,令牌传输和存储这块做得特别糙。要么明文走HTTP,要么直接塞进SharedPreferences。结果呢?被中间人一抓一个准,或者被别的恶意应用直接读走。嗯,今天咱们就把这块彻底聊透。

4.1 HTTPS/TLS:传输层的最后一道防线

说白了,HTTPS就是HTTP + TLS。TLS负责加密、身份验证和数据完整性校验。没有它,你的令牌在网络上就是裸奔。

4.1.1 为什么必须用HTTPS?

你想想看,公共Wi-Fi、运营商网关、公司路由器……这些中间节点都能看到你的网络流量。如果只用HTTP,令牌就是明文字符串,谁都能拿走。

我曾经在客户现场做过一次渗透测试。他们的移动App用的还是HTTP,我直接在同一个Wi-Fi下用Wireshark抓包,不到5分钟就把用户的登录令牌全抓到了。嗯,那次报告写得特别长。

⚠️ 警告: 绝对不要在非HTTPS连接中传输任何令牌信息。包括登录请求、令牌刷新、令牌吊销等所有涉及令牌的API调用,都必须走HTTPS。

4.1.2 TLS配置的最佳实践

光用HTTPS还不够,配置不对照样有漏洞。我整理了几个关键点:

配置项 推荐值 说明
TLS版本 TLS 1.2 / 1.3 禁用SSL 3.0、TLS 1.0、TLS 1.1
密码套件 ECDHE + AES-GCM + SHA256 优先使用前向安全的套件
证书验证 严格验证 不要跳过证书校验,不要信任自签名证书
HSTS 启用 强制浏览器/客户端只走HTTPS
证书固定 可选但推荐 防止CA被攻破后的中间人攻击

我个人习惯在移动端做证书固定(Certificate Pinning)。就是把服务器的公钥或者证书哈希直接硬编码到客户端。这样就算CA被黑了,攻击者伪造的证书也过不了校验。

💡 小技巧: 做证书固定时,建议固定2-3个备用证书。万一主证书要轮换,客户端不至于直接挂掉。我一般会固定当前证书和下一张即将启用的证书。

4.2 客户端安全存储:Keychain vs Keystore

令牌到了客户端,不能随便找个文件就存。Android和iOS都提供了专用的安全存储区域。

4.2.1 iOS Keychain

iOS的Keychain是一个加密的数据库,专门用来存敏感信息。它有几个特点:

  • 系统级加密: 数据用硬件密钥加密,即使设备越狱也很难读取
  • 应用沙箱隔离: 每个App只能访问自己的Keychain条目
  • iCloud同步: 可以配置是否同步到iCloud
  • 访问控制: 可以设置生物识别(Face ID/Touch ID)保护

我记得有个项目,客户非要把令牌存在UserDefaults里。我说不行,他问为什么。我说你拿个越狱手机试试,用iExplorer一读就出来了。后来他乖乖改成了Keychain。

4.2.2 Android Keystore

Android的Keystore系统从4.3开始引入,到了Android 6.0以上才真正可用。它利用TEE(可信执行环境)来保护密钥材料。

使用方式是这样的:

// 生成密钥
KeyPairGenerator generator = KeyPairGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
generator.initialize(
    new KeyGenParameterSpec.Builder("token_key",
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setDigests(KeyProperties.DIGEST_SHA256)
        .setUserAuthenticationRequired(true)  // 需要生物识别
        .build());
KeyPair keyPair = generator.generateKeyPair();

// 用公钥加密令牌
Cipher cipher = Cipher.getInstance("ECIES");
cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
byte[] encryptedToken = cipher.doFinal(token.getBytes());

// 存密文
sharedPreferences.edit()
    .putString("encrypted_token", Base64.encodeToString(encryptedToken, Base64.DEFAULT))
    .apply();
🔑 核心原则: 密钥永远不出Keystore硬件。你只能用它来加密/解密,但拿不到密钥本身的明文。这就是硬件隔离的优势。

4.2.3 两种方案的对比

维度 iOS Keychain Android Keystore
硬件支持 Secure Enclave TEE / StrongBox
存储内容 直接存令牌明文 存加密后的令牌
访问控制 系统级,支持生物识别 需手动配置,支持生物识别
备份安全 可配置是否参与备份 密钥不参与备份
跨App共享 通过Access Group 不支持(除非同签名)

4.3 内存保护与混淆

令牌在内存里待着的时候,也不是绝对安全的。攻击者可以通过内存dump、调试器、崩溃日志等方式把令牌捞出来。

4.3.1 内存中的令牌生命周期

我建议遵循一个原则:令牌在内存中停留的时间越短越好。用完就清掉,别留着过年。

  • 使用后立即清零: 把令牌变量赋值为空字符串或全零字节数组
  • 避免字符串常量: 字符串在Java/String中是immutable的,GC不会立即回收。用char[]或byte[],用完手动填零
  • 不要打印日志: 绝对不要把令牌写到logcat或NSLog里。我见过太多线上事故就是因为日志泄露了令牌
⚠️ 注意: Java/String的不可变性意味着你没法直接清空它。就算你把引用置为null,原来的字符串对象还在内存里,直到GC回收。所以,能用char[]就别用String。

4.3.2 代码混淆

混淆不是为了防破解,而是为了增加逆向分析的难度。攻击者看到混淆后的代码,想找到令牌相关的逻辑就得花更多时间。

Android上我用ProGuard/R8,iOS上我用LLVM的编译优化加符号剥离。具体配置:

# ProGuard 配置示例
-keepclassmembers class * {
    @com.google.gson.annotations.SerializedName <fields>;
}
-keep class com.yourapp.api.** { *; }
-keepclassmembers class * {
    native <methods>;
}

# 混淆字符串常量
-optimizationpasses 5
-allowaccessmodification
-repackageclasses ''

# 移除日志
-assumenosideeffects class android.util.Log {
    public static boolean isLoggable(java.lang.String, int);
    public static int v(...);
    public static int d(...);
    public static int i(...);
}

我曾经遇到过一个案例:某App没做混淆,攻击者直接用Jadx反编译,在代码里找到了令牌的加密密钥。嗯,那个密钥叫"secret_key_123",写在注释里。你说这能怪谁?

4.3.3 运行时保护

除了静态混淆,运行时也要防着点:

  • 反调试检测: 检测调试器是否附加,如果发现就清空令牌并退出
  • Root/Jailbreak检测: 越狱设备上内存更容易被dump,可以考虑限制功能或直接退出
  • 内存加密: 对敏感数据做内存级的加密,用的时候解密,用完立即销毁密钥
💡 我的习惯: 在App启动时生成一个临时密钥,存在Native层(JNI/FFI)。所有令牌操作都用这个密钥做内存加密。App切到后台时,立即销毁密钥和所有令牌副本。回到前台再重新获取。

4.4 整体架构图

下面这张图展示了令牌从传输到存储再到内存使用的完整安全链路:

用户令牌安全传输与存储架构 客户端(App) 服务器 TLS 1.2/1.3 证书固定 安全存储 iOS Keychain / Android Keystore 内存保护 临时密钥 + 手动清零 代码混淆 ProGuard / 符号剥离 运行时保护 反调试 / Root检测 令牌验证 签名校验 / 过期检查 安全存储 HSM / 加密数据库 审计日志 令牌使用记录 / 异常检测 吊销机制 黑名单 / 实时失效 双向TLS加密

这张图把整个链路分成了三层:传输层用TLS加密,客户端用Keychain/Keystore做持久化存储,内存里用临时密钥加手动清零。每一层都防着不同的攻击面。

嗯,做到这个程度,你的令牌基本就安全了。但记住,安全是个持续对抗的过程。攻击者在进步,你的防护也得跟着升级。