第4章 Android端OkHttp证书固定:配置CertificatePinner、支持多个公钥、备份公钥机制
4.1 为什么需要多个公钥与备份公钥
单一公钥固定存在单点失效风险:一旦私钥泄露或证书轮换,应用将无法连接服务器,导致服务中断。通过配置多个公钥指纹(multiple pins)和备份公钥(backup pins),可以实现无中断平滑迁移,既提升安全性又保障可用性。
| 配置类型 | 描述 | 适用场景 |
|---|---|---|
| 单个公钥 | 仅固定一个SPKI指纹,简单但脆弱 | 开发、测试环境 |
| 多个公钥 | 固定2‑3个指纹(如当前证书+备用证书) | 生产环境,允许同时使用多张证书 |
| 备份公钥机制 | 主用公钥组 + 备份公钥组,按优先级匹配 | 证书轮换、紧急回滚场景 |
4.2 原理:CertificatePinner的工作流程
OkHttp的CertificatePinner在check()方法中执行以下逻辑:
- 提取服务端证书链中每个证书的SubjectPublicKeyInfo,计算SHA‑256哈希。
- 与配置的pins列表(同一域名下所有指纹)逐一匹配。
- 如果至少有一个证书的指纹匹配任意一个pin,则通过;否则抛出
SSLPeerUnverifiedException。 - 备份公钥实现方式:在同一个域名下配置多个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的固定检查流程:
图中展示了主pin列表(Primary)和备份pin列表(Backup)的组合检查逻辑:先匹配主列表,如果失败则继续尝试备份列表,全部失败后才拒绝连接。实际代码中,只需将主用和备份指纹都添加到同一.add()调用中,OkHttp按host顺序逐条匹配,逻辑等价于上述流程。
4.9 小结
通过在OkHttp中配置CertificatePinner的多个公钥及备份公钥,可以显著提高证书固定的健壮性。部署时建议:始终保留至少两个指纹(旧+新);使用远程配置下发最新指纹列表以支持热更新;结合日志和异常监控及时发现证书不匹配问题。备份机制不需要额外库或复杂逻辑,合理规划pin列表即可实现平滑轮换。