22. 网络安全策略:网络安全配置、明文流量限制、证书锁定
聊到Android安全,很多人第一反应是权限、沙箱这些。但说实话,网络这块才是真正的前线战场。你想想看,一个App每天要发多少请求出去?这些数据一旦在传输过程中被截获,那后果...
我早年做过一个金融类项目,当时团队对网络安全的重视程度不够,觉得「反正用HTTPS就行了」。结果呢?上线没多久就被安全团队打回,说存在中间人攻击风险。嗯,从那以后,我对网络安全策略这块就格外上心。
22.1 网络安全配置:你的第一道防线
Android从6.0开始引入了网络安全配置(Network Security Config)。这东西说白了,就是让你用一个XML文件,集中管理App的所有网络行为。
我个人习惯,每个新项目的第一步,就是在AndroidManifest.xml里加上这个配置:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
然后在 res/xml/network_security_config.xml 里,你可以做很多事情:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>
核心要点:
- base-config:全局默认策略,所有域名都适用
- domain-config:针对特定域名的例外规则
- debug-overrides:仅调试模式下生效,千万别用到正式包
这里有个坑,我曾经见过有人把调试用的证书配置直接带到正式包。结果呢?用户的数据可以被任意中间人解密。这可不是闹着玩的。
22.2 明文流量限制:为什么必须关掉HTTP
明文流量,就是HTTP请求。数据在网络上裸奔,谁都能看。Android 9开始,默认就禁止了明文流量。但很多老项目还在用,或者开发图方便临时开了。
我建议你这样做:
- 全局关闭:在base-config里设置
cleartextTrafficPermitted="false" - 例外处理:只有内网测试环境才允许HTTP,且要严格限定IP范围
- 代码层面:如果用了OkHttp,可以加拦截器做二次校验
// OkHttp 拦截器示例
class ClearTextBlockerInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.scheme == "http") {
// 记录日志,或者直接抛异常
Log.w("Security", "明文请求被拦截: ${request.url}")
// 生产环境建议直接拒绝
throw IOException("明文流量已被安全策略禁止")
}
return chain.proceed(request)
}
}
注意:AndroidManifest里的 android:usesCleartextTraffic 属性虽然也能控制,但粒度太粗。网络安全配置才是正道。
22.3 证书锁定:对抗中间人攻击的终极武器
证书锁定(Certificate Pinning),说白了就是让你的App只信任特定的服务器证书。就算攻击者伪造了证书,只要不是我们锁定的那张,连接就会失败。
我记得有一次帮客户做安全审计,发现他们的App虽然用了HTTPS,但没有任何证书校验。我当场演示了中间人攻击,直接抓包看到了用户的登录密码。客户脸都绿了。
实现证书锁定,常用的方式有两种:
| 方式 | 原理 | 优缺点 |
|---|---|---|
| 公钥哈希锁定 | 锁定服务器证书的公钥哈希值 | 证书更换时只需更新公钥,灵活性好 |
| 证书直接锁定 | 锁定完整的证书内容 | 安全性最高,但证书过期后必须更新App |
我个人更推荐公钥哈希锁定。为什么?因为证书会过期,但公钥可以保持不变。你想想看,如果锁定了完整证书,每年换证时都得发版,多麻烦。
OkHttp实现证书锁定非常方便:
// 公钥哈希锁定示例
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.build()
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
小技巧:开发阶段可以开启 certificatePinner.check() 的调试模式,它会告诉你当前服务器返回的证书哈希值。这样你就不用自己去算了。
22.4 知识体系总览
下面这张图,是我梳理的网络安全策略核心逻辑。你可以看到,这三层策略是层层递进的:
22.5 实战中的避坑指南
做了这么多年安全,我总结了几条血泪教训:
- 不要硬编码证书哈希:我曾经见过有人把哈希值直接写在代码里,结果证书一换,App全崩。建议从远程配置拉取。
- 做好备份策略:证书锁定太死,万一服务器换证而你来不及发版,用户就全连不上了。至少锁定2-3个备用证书。
- 调试和正式环境分开:调试时可以用
network_security_config_debug.xml,正式包用network_security_config.xml。千万别混。 - 定期审计:网络安全配置不是写完就完事了。我建议每个版本都检查一遍,看看有没有不小心开了明文流量。
一句话总结:
网络安全配置是基础,明文流量限制是底线,证书锁定是终极保障。三层都做到位,你的App在网络层面才算真正安全。
好了,这一章的内容就到这里。记住,网络安全不是功能,是态度。你对待安全的态度,决定了用户数据的安全程度。
公众号:蓝海资料掘金营,微信deep3321