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开始,默认就禁止了明文流量。但很多老项目还在用,或者开发图方便临时开了。

我建议你这样做:

  1. 全局关闭:在base-config里设置 cleartextTrafficPermitted="false"
  2. 例外处理:只有内网测试环境才允许HTTP,且要严格限定IP范围
  3. 代码层面:如果用了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 知识体系总览

下面这张图,是我梳理的网络安全策略核心逻辑。你可以看到,这三层策略是层层递进的:

Android网络安全策略三层架构 第一层:网络安全配置 定义全局策略,控制明文流量、信任锚点 network_security_config.xml → base-config / domain-config 第二层:明文流量限制 强制HTTPS,拦截所有HTTP请求 cleartextTrafficPermitted="false" + 代码拦截器 第三层:证书锁定 对抗中间人攻击,只信任指定证书 公钥哈希锁定 / 证书直接锁定 → CertificatePinner

22.5 实战中的避坑指南

做了这么多年安全,我总结了几条血泪教训:

  • 不要硬编码证书哈希:我曾经见过有人把哈希值直接写在代码里,结果证书一换,App全崩。建议从远程配置拉取。
  • 做好备份策略:证书锁定太死,万一服务器换证而你来不及发版,用户就全连不上了。至少锁定2-3个备用证书。
  • 调试和正式环境分开:调试时可以用 network_security_config_debug.xml,正式包用 network_security_config.xml。千万别混。
  • 定期审计:网络安全配置不是写完就完事了。我建议每个版本都检查一遍,看看有没有不小心开了明文流量。

一句话总结:

网络安全配置是基础,明文流量限制是底线,证书锁定是终极保障。三层都做到位,你的App在网络层面才算真正安全。

好了,这一章的内容就到这里。记住,网络安全不是功能,是态度。你对待安全的态度,决定了用户数据的安全程度。


公众号:蓝海资料掘金营,微信deep3321