第4章 Android端OkHttp证书固定:配置CertificatePinner、支持多个公钥、备份公钥机制

4.1 为什么需要多个公钥与备份公钥

单一公钥固定存在单点失效风险:一旦私钥泄露或证书轮换,应用将无法连接服务器,导致服务中断。通过配置多个公钥指纹(multiple pins)和备份公钥(backup pins),可以实现无中断平滑迁移,既提升安全性又保障可用性。

配置类型 描述 适用场景
单个公钥 仅固定一个SPKI指纹,简单但脆弱 开发、测试环境
多个公钥 固定2‑3个指纹(如当前证书+备用证书) 生产环境,允许同时使用多张证书
备份公钥机制 主用公钥组 + 备份公钥组,按优先级匹配 证书轮换、紧急回滚场景

4.2 原理:CertificatePinner的工作流程

OkHttp的CertificatePinner在check()方法中执行以下逻辑:

  1. 提取服务端证书链中每个证书的SubjectPublicKeyInfo,计算SHA‑256哈希。
  2. 与配置的pins列表(同一域名下所有指纹)逐一匹配。
  3. 如果至少有一个证书的指纹匹配任意一个pin,则通过;否则抛出SSLPeerUnverifiedException。
  4. 备份公钥实现方式:在同一个域名下配置多个pin(包括当前主用和备用的指纹),或为域名指定通配符匹配(如*.example.com)并在不同域名下配置不同pin,实现优先级。
关键点:每次TLS握手都会执行一次证书固定检查。OkHttp内部维护了会话缓存(默认最多20个),但对不同host的pin需独立配置。

4.3 实现流程:在OkHttp中配置多公钥与备份公钥

  • 步骤1:添加OkHttp依赖(Gradle或Maven)。
  • 步骤2:计算服务器证书公钥的SHA‑256指纹(可用openssl或在线工具)。
  • 步骤3:构建CertificatePinner对象,为每个域名添加多个pin。
  • 步骤4:将CertificatePinner设置到OkHttpClient.Builder。
  • 步骤5:客户端发起请求,验证证书是否匹配。

代码示例:配置多个公钥与备份公钥

// 假设当前证书指纹:sha256/AAAA...,备用证书指纹:sha256/BBBB...
// 第二个域名使用另一组指纹作为备份
CertificatePinner certificatePinner = new CertificatePinner.Builder()
    .add("api.payment.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .add("api.payment.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
    .add("*.payment.example.com", "sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=")  // 通配符
    .add("backup-api.payment.example.com", "sha256/DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD=") // 备份备用域名
    .build();

OkHttpClient client = new OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build();
备份机制本质:将不同时期、不同域名或不同CA签发的公钥指纹同时加入同一域名的pin列表。OkHttp按host匹配,只要任意一个pin命中即通过。因此备份公钥不需要特殊语法,只需在.add()中列出所有可接受的指纹即可。

4.4 调试:验证证书固定是否生效

  • 使用logging-interceptor观察TLS握手日志。
  • 故意配置一个错误的指纹(如sha256/wrong=),确认客户端抛出异常:
javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
  Peer certificate chain does not match any pinned certificate.
  • 通过CertificatePinner.getPins()可以查看配置的所有pin。
注意:调试时避免在生产环境使用错误指纹,防止线上应用崩溃。建议在测试环境验证后再部署。

4.5 部署注意事项

项目 建议 原因
存储方式 将指纹列表存储在App内置或安全远程配置中 便于热更新,不需重新发版
备份公钥数量 建议 2‑3 个(主用 + 1‑2个备用) 过多会增加维护复杂度,过少则失去备份意义
更新策略 新旧指纹共存一段时间(例如 30天) 保证所有客户端都完成更新前,旧指纹仍可用
例外放行 不要对自签名证书或内部CA直接放行;务必使用真实公钥指纹 避免中间人攻击绕过

4.6 常见错误

  • 指纹格式错误:必须使用sha256/前缀,Base64编码长度44字符(不含填充)。
  • Host不匹配:域名严格区分大小写,通配符*只能匹配一个标签,不能跨级。
  • 未包含备份公钥:证书轮换时应用直接断连。部署前确保至少配置了新旧两个指纹。
  • 混淆导致计算指纹错误:从客户端预埋了错误指纹。建议从服务器端获取证书后,在客户端用openssl再次验证。

4.7 性能与稳定性优化

  • 重用OkHttpClient实例:CertificatePinner作为不变对象,避免每次请求重新构建。
  • 使用长连接Keep-Alive:降低TLS握手频率,节省证书固定计算开销。
  • 异步预加载指纹列表:从远程配置中心获取最新备份公钥,启动时缓存,避免阻塞网络请求。
  • 限制固定检查的域名范围:仅对支付类API域名启用证书固定,非敏感域名保持常规验证。
  • 监控告警:对证书固定失败日志进行采集,及时发现服务器证书异常或恶意攻击。

4.8 多公钥与备份公钥机制原理图

下图展示了客户端请求api.payment.example.com时,OkHttp的固定检查流程:

多公钥与备份公钥机制流程图 OkHttp证书固定:多公钥与备份公钥流程 发起HTTPS请求 获取服务端证书链 计算证书SPKI SHA256 匹配Primary Pins列表 匹配成功 ✅ 匹配Backup Pins列表 任一Pin匹配 → 通过,否则拒绝 配置方式 .add("host", "sha256/AAA") .add("host", "sha256/BBB") //备份 .add("*.host", "sha256/CCC") //通配 备份:仅增加额外pin即可

图中展示了主pin列表(Primary)和备份pin列表(Backup)的组合检查逻辑:先匹配主列表,如果失败则继续尝试备份列表,全部失败后才拒绝连接。实际代码中,只需将主用和备份指纹都添加到同一.add()调用中,OkHttp按host顺序逐条匹配,逻辑等价于上述流程。

4.9 小结

通过在OkHttp中配置CertificatePinner的多个公钥及备份公钥,可以显著提高证书固定的健壮性。部署时建议:始终保留至少两个指纹(旧+新);使用远程配置下发最新指纹列表以支持热更新;结合日志和异常监控及时发现证书不匹配问题。备份机制不需要额外库或复杂逻辑,合理规划pin列表即可实现平滑轮换。