安全实践:HTTPS与证书锁定、防止中间人攻击、敏感信息加密
网络层安全,说白了就是你的App和服务器之间的对话不能被偷听、篡改或冒充。我见过太多团队把精力全放在业务逻辑上,结果上线第一天就被抓包工具扒了个底朝天。今天咱们就聊聊怎么把网络层真正锁死。
一、HTTPS不是终点,是起点
很多人觉得上了HTTPS就万事大吉了。嗯,这个想法很危险。HTTPS确实加密了传输通道,但中间人攻击(MITM)依然能绕过它。你想想看,如果用户手机上装了个伪造的CA证书,或者攻击者控制了WiFi热点,HTTPS照样能被解密。
我在项目中遇到过一件事:某金融App上线后,用户反馈登录异常。一查才发现,有第三方SDK偷偷替换了系统的证书信任链。从那以后,我对网络层的安全策略做了彻底重构。
二、证书锁定(Certificate Pinning)
证书锁定,说白了就是让你的App只信任你指定的那个证书,而不是系统里所有的CA证书。这样就算攻击者伪造了证书,只要不是你的那张,连接就会失败。
2.1 锁定方式对比
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 公钥锁定 | 锁定服务器证书的公钥 | 证书更换时公钥不变,灵活性高 | 需要提前获取公钥指纹 |
| 证书锁定 | 锁定完整的证书内容 | 最严格,安全性最高 | 证书更换时必须更新App |
| Hash锁定 | 锁定证书或公钥的SHA-256哈希 | 存储空间小,比较速度快 | 需要定期更新哈希值 |
我个人习惯用公钥锁定。为什么呢?因为证书可能会过期、被吊销,但公钥一般不会变。你想想看,如果锁死了证书,哪天证书到期了,用户不更新App就用不了,这体验太差了。
2.2 OkHttp中的证书锁定实现
在Android里,我们用的Retrofit底层是OkHttp。OkHttp原生支持证书锁定,代码写起来很简洁。
// 1. 获取服务器证书的公钥哈希值
// 可以用openssl命令生成:
// openssl s_client -connect yourdomain.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
// 2. 在OkHttp中配置CertificatePinner
val certificatePinner = CertificatePinner.Builder()
.add("yourdomain.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("yourdomain.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 备用证书
.build()
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
// 3. 创建Retrofit实例
val retrofit = Retrofit.Builder()
.baseUrl("https://yourdomain.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
2.3 调试与发布分离
调试阶段和发布阶段的证书锁定策略应该不同。调试时你可能需要抓包,锁死了反而麻烦。
// 调试模式:不锁定,方便抓包
// 发布模式:严格锁定
val certificatePinner = if (BuildConfig.DEBUG) {
CertificatePinner.Builder().build() // 空锁,不校验
} else {
CertificatePinner.Builder()
.add("yourdomain.com", "sha256/生产环境的哈希值")
.build()
}
三、防止中间人攻击的完整方案
中间人攻击(MITM)是网络层最大的威胁之一。攻击者伪装成服务器,你的App把数据发给它,它再转发给真实服务器。整个过程你完全不知情。
3.1 多层防护策略
- 证书锁定:上面已经讲过了,这是第一道防线
- Hostname验证:确保连接的主机名和证书中的一致
- SSL/TLS版本限制:禁用不安全的TLS 1.0/1.1,只允许TLS 1.2+
- 加密协议协商:使用强密码套件,禁用RC4、DES等弱加密
3.2 OkHttp的安全配置
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.hostnameVerifier { hostname, session ->
// 自定义Hostname验证
// 默认实现已经够用,除非你有特殊需求
HttpsURLConnection.getDefaultHostnameVerifier()
.verify(hostname, session)
}
.connectionSpecs(listOf(
ConnectionSpec.MODERN_TLS, // 只使用现代TLS配置
ConnectionSpec.COMPATIBLE_TLS // 兼容模式
))
.build()
核心要点: 不要自己实现HostnameVerifier返回true!我见过有人图省事直接写return true,结果整个HTTPS防护形同虚设。除非你清楚自己在做什么,否则用默认实现就好。
四、敏感信息加密
网络传输加密了,但数据到了客户端怎么办?SharedPreferences明文存储?SQLite不加密?这些都是安全隐患。
4.1 本地存储加密
Android提供了EncryptedSharedPreferences和EncryptedFile,基于Android Keystore系统,密钥由硬件安全模块保护。
// 使用EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 存储敏感信息
sharedPreferences.edit()
.putString("access_token", "your_token_here")
.apply()
4.2 网络请求中的敏感数据加密
有些数据即使走HTTPS,我也建议额外加密。比如用户的身份证号、银行卡号。为什么?因为HTTPS只保护传输过程,不保护服务器端。万一服务器被拖库,至少数据是加密的。
// 使用AES-GCM对敏感字段加密
fun encryptSensitiveData(plainText: String, secretKey: SecretKey): String {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encrypted = cipher.doFinal(plainText.toByteArray())
// 将IV和密文拼接,方便解密
val combined = ByteArray(iv.size + encrypted.size)
System.arraycopy(iv, 0, combined, 0, iv.size)
System.arraycopy(encrypted, 0, combined, iv.size, encrypted.size)
return Base64.encodeToString(combined, Base64.NO_WRAP)
}
// 在Retrofit的Converter或Interceptor中调用
class EncryptionInterceptor(
private val secretKey: SecretKey
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val body = request.body
// 对请求体中的敏感字段加密
// 具体实现取决于你的数据格式
return chain.proceed(request)
}
}
五、知识体系总览
下面这张图总结了网络层安全的核心逻辑,从传输层到存储层,每一层都不能漏。
六、避坑指南
最后分享几个我踩过的坑,希望能帮你少走弯路。
- 不要信任用户输入的任何证书:有些App允许用户导入自签名证书,这等于把安全大门打开了。除非是内部企业应用,否则别这么做。
- 证书锁定要留后路:我建议至少锁定3个哈希值——当前证书、备用证书、根证书。万一主证书出问题,还有回旋余地。
- 定期更新锁定配置:证书会过期,公钥也可能变更。我习惯在App启动时检查远程配置,如果服务器端更新了证书,客户端能及时同步。
- 日志里别打印敏感信息:调试时打印请求体很常见,但记得在Release包中关闭。我曾经见过生产环境的日志里明文打印了用户密码,那画面太美不敢看。
好了,这一章的内容就到这里。网络层安全说难不难,说简单也不简单。关键是要有「纵深防御」的意识——不要依赖单一防护手段,每一层都加固,才能真正挡住攻击者。