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分钟就把用户的登录令牌全抓到了。嗯,那次报告写得特别长。
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被黑了,攻击者伪造的证书也过不了校验。
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();
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里。我见过太多线上事故就是因为日志泄露了令牌
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,可以考虑限制功能或直接退出
- 内存加密: 对敏感数据做内存级的加密,用的时候解密,用完立即销毁密钥
4.4 整体架构图
下面这张图展示了令牌从传输到存储再到内存使用的完整安全链路:
这张图把整个链路分成了三层:传输层用TLS加密,客户端用Keychain/Keystore做持久化存储,内存里用临时密钥加手动清零。每一层都防着不同的攻击面。
嗯,做到这个程度,你的令牌基本就安全了。但记住,安全是个持续对抗的过程。攻击者在进步,你的防护也得跟着升级。