HTTPS 与 TLS 基础配置:SSLSocketFactory、HostnameVerifier、证书校验
在 OkHttp 接口防护中,HTTPS 不仅依赖 URL 前缀,还需要在底层配置 SSL/TLS 握手参数。OkHttp 通过 SSLSocketFactory 自定义加密套件和协议版本,通过 HostnameVerifier 校验服务器域名与证书的匹配性,并支持双向证书校验。本章从原理出发,给出完整的配置流程、代码示例、调试方法及常见错误处理。
3.1 核心概念与原理
TLS 握手阶段,客户端需要确认服务器身份(单向认证),或双方相互认证(双向认证)。OkHttp 将这些控制权委托给三个关键组件:
| 组件 | 职责 | 默认行为 |
|---|---|---|
| SSLSocketFactory | 创建 SSL/TLS 套接字,决定协议版本(TLSv1.2/v1.3)、密码套件、信任管理器(TrustManager) | 使用平台默认工厂,支持 TLS 1.2~1.3 |
| HostnameVerifier | 校验证书中 CN/SAN 字段是否匹配实际请求的 host | OkHttp 内置 OkHostnameVerifier,严格匹配 |
| 证书校验(CertificatePinner/X509TrustManager) | 限定只信任特定证书或 CA,防止中间人攻击 | 信任系统受信任的 CA |
// 典型 TLS 握手核心逻辑(伪代码)
SSLSocketFactory factory = SSLContext.getInstance("TLSv1.3") // 协议版本
.init(keyManager, trustManager, secureRandom)
.getSocketFactory();
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(factory, trustManager) // 提供信任管理器
.hostnameVerifier(new StrictHostnameVerifier())
.build();
关键原则:不要信任所有证书 (
ALLOW_ALL_HOSTNAME_VERIFIER),不要使用过时协议(SSLv3、TLSv1.0),生产环境必须显式配置 TrustManager。
设计思想:OkHttp 将底层 TLS 实现完全开放,开发者可以自定义证书库、密钥库或使用
CertificatePinner 直接锁定公钥指纹,实现细粒度安全策略。
3.2 实现流程与代码示例
以下从最严格的配置开始,逐步给出实际代码。
3.2.1 自定义 SSLSocketFactory(限用 TLSv1.3、指定密码套件)
// 创建安全 SSLContext,只启用 TLSv1.3
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
TrustManager[] trustManagers = new TrustManager[]{
new X509TrustManager() {
@Override public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}
};
sslContext.init(null, trustManagers, new SecureRandom());
// 获取 SSLSocketFactory
SSLSocketFactory factory = sslContext.getSocketFactory();
// 应用到 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(factory, (X509TrustManager) trustManagers[0])
.hostnameVerifier(OkHostnameVerifier.INSTANCE)
.build();
3.2.2 自定义 HostnameVerifier(拒绝域名不匹配)
OkHttpClient client = new OkHttpClient.Builder()
.hostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// 只允许特定域名通过校验
return hostname.equals("api.secure-example.com")
&& session.getPeerPrincipal() != null;
}
})
.build();
安全提示:不要在生产中使用
NO_VERIFY 或 ALLOW_ALL 的 HostnameVerifier,那将使 HTTPS 降级为纯加密通道,无认证保护。
3.2.3 证书锁定(CertificatePinner)
// 方式一:锁定公钥指纹(SHA-256 base64)
CertificatePinner pinner = new CertificatePinner.Builder()
.add("api.secure-example.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // 示例指纹
.add("api.secure-example.com",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // 备用指纹
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(pinner)
.build();
3.3 内联 SVG:TLS 握手与证书校验流程
下图为 OkHttp 启用自定义 SSL 配置后的完整握手与校验流程:
3.4 调试与日志
启用 OkHttp 的 Wire 级别日志可观察 TLS 握手详情:
// 在 OkHttpClient 构建时添加拦截器
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
// 或使用 BODY 级别查看证书详情
client = client.newBuilder()
.addInterceptor(logging)
.build();
TLS 握手阶段的常见日志输出:
D/OkHttp: <-- 200 OK https://api.example.com/path (325ms)
D/OkHttp: Connection: keep-alive
D/OkHttp: Content-Type: application/json
D/OkHttp:
D/OkHttp: {"status":"ok"}
若发生握手失败,日志会包含 javax.net.ssl.SSLHandshakeException 及具体原因,如证书链不完整、域名不匹配、证书过期或未知 CA。
3.5 部署注意事项
- 证书链完整:服务器须发送完整链(包含中间证书),否则 Android 4.x 以下设备可能报错。
- 证书更换:CertificatePinner 锁定公钥时,至少保留一个备用指纹(新证书指纹),避免到期前服务中断。
- 兼容性:Android 5.0+ 原生支持 TLSv1.2,建议在
AndroidManifest.xml的<application>中添加android:networkSecurityConfig作为兜底策略。
部署清单:
✅ 显式设置 TLSv1.2/TLSv1.3
✅ 使用 OkHostnameVerifier.INSTANCE(或自定义严格规则)
✅ 生产环境至少锁定 2 个公钥指纹
✅ 配置网络安全性 XML(仅用于非受控场景)
✅ 显式设置 TLSv1.2/TLSv1.3
✅ 使用 OkHostnameVerifier.INSTANCE(或自定义严格规则)
✅ 生产环境至少锁定 2 个公钥指纹
✅ 配置网络安全性 XML(仅用于非受控场景)
3.6 常见错误与解决方案
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
SSLPeerUnverifiedException: Hostname not verified | 域名与证书 CN/SAN 不匹配 | 使用正确的域名,或自定义 HostnameVerifier 放宽校验(谨慎) |
SSLHandshakeException: Not trusted server certificate | 证书不被信任管理器接受(自签/未知 CA) | 添加对应 CA 证书到 TrustManager,或使用 CertificatePinner 直接锁定公钥 |
SSLException: Connection closed by peer | 服务器不支持客户端发送的协议版本/密码套件 | 在 SSLSocketFactory 中启用兼容的密码套件(例如加入 TLS_AES_128_GCM_SHA256) |
java.security.KeyStoreException: Trusted certificate entry is not a trusted certificate | 加载的 BKS/KeyStore 格式错误或密码错误 | 使用 KeyStore.getInstance("BKS") 并指定正确的密码 |
3.7 性能与稳定性优化
- 复用 SSLSocketFactory:创建 SSLContext 非常昂贵,应当作为单例缓存,所有请求共用同一个工厂。
- 避免每次请求建立新连接:OkHttp 默认开启连接池,如需自定义 TLS 参数,确保使用
ConnectionPool复用。 - 协议降级:某些老旧服务器只支持 TLSv1.1,此时需在
SSLContext.getInstance("TLS")后通过SSLParameters设置setProtocols(new String[]{"TLSv1.1","TLSv1.2"}),但必须设置超时时间,避免因协议协商导致慢连接。 - 证书固定更新策略:使用远程配置下发最新的指纹列表,避免升级客户端才能更换证书。
// 推荐:全局单例 SSLContext
public class SSLContextHolder {
private static final SSLContext SSL_CONTEXT = createContext();
private static SSLContext createContext() {
try {
SSLContext ctx = SSLContext.getInstance("TLSv1.3");
ctx.init(null, new TrustManager[]{new MyTrustManager()}, null);
return ctx;
} catch (Exception e) {
throw new RuntimeException("Failed to initialize SSLContext", e);
}
}
public static SSLSocketFactory getSocketFactory() {
return SSL_CONTEXT.getSocketFactory();
}
}
稳定性提醒:自定义 TrustManager 时,若
checkServerTrusted 方法实现为空,将绕过所有证书校验,极端危险。务必实现完整的 X509 校验逻辑或使用标准 TrustManagerFactory。