安全攻防 - 07 深入接入:Kona Provider、SSLContext 与 OkHttp 兼容层
文章目录
- 1. Java 安全体系的几个关键词
- 2. 为什么要三个 Kona Provider
- 3. `client-signature-schemes` 为什么放在 Provider 初始化阶段
- 4. TrustManager 构造链路详解
- 5. KeyManager 构造链路详解
- 6. SSLContext 构造
- 7. 为什么 Forest 默认 SSL 管道不够
- 8. OkHttp 关卡 1:ConnectionSpec
- 9. OkHttp 关卡 2:Handshake.get 解析协议名
- 10. 协议名 mask 的设计
- 11. 这个兼容层什么时候能删
- 12. 为什么诊断路径要重新 build SSLContext
- 13. 迁移到别的项目时最少要搬哪些东西
- 14. 本篇小结

前面已经能跑请求了。这一篇解释“为什么代码要这么写”。如果你后面要把这套逻辑迁到另一个项目,或者升级 Forest/OkHttp/Kona,这篇最关键。
1. Java 安全体系的几个关键词
Java 不是把所有加密算法都写死在 JDK 里,而是通过 Provider 机制扩展。
常见对象:
| 对象 | 作用 |
|---|---|
Provider |
安全能力提供者,比如 KonaSSL |
Security.addProvider |
把 Provider 注册进 JVM |
CertificateFactory |
解析证书 |
KeyStore |
存证书/私钥/信任锚 |
TrustManagerFactory |
根据 TrustStore 生成 TrustManager |
KeyManagerFactory |
根据 KeyStore 生成 KeyManager |
SSLContext |
具体协议的 SSL/TLS/TLCP 上下文 |
SSLSocketFactory |
从 SSLContext 创建安全 socket |
本工程就是按这条链路组装。
2. 为什么要三个 Kona Provider
KonaProviderInitializer 注册:
addIfAbsent(KonaCryptoProvider.instance());
addIfAbsent(KonaPKIXProvider.instance());
addIfAbsent(KonaSSLProvider.instance());
2.1 KonaCrypto
提供 SM2、SM3、SM4 等基础密码算法。
没有它,底层签名、验签、加密、解密、摘要等操作可能找不到算法实现。
2.2 KonaPKIX
提供国密 X.509/PKIX 相关能力。
本工程里用它做:
KeyStore.getInstance("JKS", "KonaPKIX")
CertificateFactory.getInstance("X.509", "KonaPKIX")
KeyStore.getInstance("PKCS12", "KonaPKIX")
也就是说,读取 SM2 证书、把 CA 放进 TrustStore、读取客户端 P12/JKS,都依赖它。
2.3 KonaSSL
提供 TLCP/国密 SSL 能力。
本工程里用它做:
TrustManagerFactory.getInstance("PKIX", "KonaSSL")
KeyManagerFactory.getInstance("NewSunX509", "KonaSSL")
SSLContext.getInstance("TLCPv1.1", "KonaSSL")
没有它,Java 默认 SSLContext 不认识 TLCPv1.1。
3. client-signature-schemes 为什么放在 Provider 初始化阶段
代码:
public static final String KONA_CLIENT_SIGNATURE_SCHEMES_PROPERTY =
"com.tencent.kona.ssl.client.signatureSchemes";
初始化时:
System.setProperty(KONA_CLIENT_SIGNATURE_SCHEMES_PROPERTY, "sm2sig_sm3");
这个属性影响 ClientHello 里通告的签名算法。
它必须尽早设置,最好在创建 SSLContext 前完成。否则 SSLContext/握手逻辑可能已经读取了旧值。
本工程做了两层保险:
KonaProviderInitializer有@PostConstruct;TlcpSslContextFactory.buildSslContext()里再次调用providerInitializer.initialize()。
这可以避免测试或手工 new 工厂时 Provider 没注册。
4. TrustManager 构造链路详解
代码路径:TlcpSslContextFactory.buildTrustManagers(...)。
4.1 必须至少有一张 CA
if (CollectionUtils.isEmpty(certificateLocations)) {
throw new IllegalStateException("gm.tlcp.tls.trust-certificates must contain at least one CA certificate");
}
这是安全边界:不允许没有信任锚还继续握手。
4.2 创建空 TrustStore
KeyStore trustStore = KeyStore.getInstance("JKS", "KonaPKIX");
trustStore.load(null, null);
这是运行时临时 TrustStore,不是磁盘上的 cacerts。
4.3 解析每个证书
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", "KonaPKIX");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(inputStream);
用 generateCertificates 而不是 generateCertificate 的好处:一个 PEM 文件里如果拼了多张证书,也能一次读取。
4.4 放入 TrustStore
trustStore.setCertificateEntry("trusted-ca-" + certIndex, x509Certificate);
alias 只是 KeyStore 内部名字,不影响证书链逻辑。
4.5 生成 TrustManager
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(trustManagerAlgorithm, "KonaSSL");
trustManagerFactory.init(trustStore);
return trustManagerFactory.getTrustManagers();
重点:TrustManagerFactory 用 KonaSSL,不是默认 JSSE。
5. KeyManager 构造链路详解
只有配置了客户端 KeyStore 才走:
if (clientKeyStore == null || !clientKeyStore.isConfigured()) {
return null;
}
加载流程:
Resource resource = resolveResource(clientKeyStore.getPath());
KeyStore keyStore = KeyStore.getInstance(clientKeyStore.getType(), "KonaPKIX");
keyStore.load(inputStream, clientKeyStore.storePassword());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyManagerAlgorithm, "KonaSSL");
kmf.init(keyStore, clientKeyStore.keyPassword());
return kmf.getKeyManagers();
如果服务端不要求客户端证书,SSLContext.init(null, trustManagers, random) 是合法的。
6. SSLContext 构造
核心:
SSLContext sslContext = SSLContext.getInstance(contextProtocol, "KonaSSL");
sslContext.init(keyManagers, trustManagers, new SecureRandom());
其中 contextProtocol 来自:
- 请求表单的
tlsProtocol; - 配置的
gm.tlcp.tls.protocol; - 兜底
TLCPv1.1。
生产不要随便改成 TLS。如果对方是 TLCP,就必须让 SSLContext 使用 TLCP 协议实现。
7. 为什么 Forest 默认 SSL 管道不够
Forest 是 HTTP 客户端封装。它能很好地构造请求,但底层 OkHttp 默认假设是普通 TLS。
本工程没有简单配置 Forest SSL,而是在每个请求上注入自定义 OkHttpClient:
forestConfiguration.post(properties.uploadUrl())
.backendClient(okHttpClient)
.contentTypeMultipartFormData()
.addHeader("Content-Meta", contentMetaJson)
.addFile("file", file)
这样可以完全控制:
SSLSocketFactory;X509TrustManager;HostnameVerifier;ConnectionSpec;- 超时时间;
- 重定向策略。
8. OkHttp 关卡 1:ConnectionSpec
OkHttp 3.x 内置 MODERN_TLS 面向普通 TLS,默认不会接受:
TLCPv1.1
TLCP_ECC_SM4_GCM_SM3
TLCP_ECC_SM4_CBC_SM3
所以要手动:
ConnectionSpec tlcpSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(enabledProtocols.toArray(new String[0]))
.cipherSuites(enabledCipherSuites.toArray(new String[0]))
.build();
注意这里用的是字符串数组。这样 TLCPv1.1 和 TLCP cipher suite 名称可以进入 OkHttp 配置。
如果缺这一步,常见错误是:
Unable to find acceptable protocols
或者还没真正握手就被 OkHttp 拒绝。
9. OkHttp 关卡 2:Handshake.get 解析协议名
即使握手成功,OkHttp 3.x 还会在事后读取 SSLSession:
session.getProtocol()
它只认识普通 TLS 枚举名。真实 TLCP 会话返回:
TLCPv1.1
于是可能抛:
Unexpected TLS version: TLCPv1.1
这非常迷惑:底层握手可能已经成功,但 OkHttp 在“记账”阶段崩了。
10. 协议名 mask 的设计
本工程的方案:
真实 Kona SSLSocketFactory
↓
RecordingSslSocketFactory(可选,用于诊断)
↓
TlcpProtocolMaskingSslSocketFactory
↓
OkHttpClient.sslSocketFactory(...)
TlcpProtocolMaskingSslSession 只改一个行为:
@Override
public String getProtocol() {
return "TLSv1.2";
}
其他方法全部透传给真实 session:
getCipherSuite()仍是真实 TLCP cipher;getPeerCertificates()仍是真实服务端证书;getId()仍是真实 session id;- 数据流仍然是 TLCP。
为什么用 TLSv1.2?因为 OkHttp 3.x 认识它。这里不是把协议降级成 TLS 1.2,只是让 OkHttp 的握手对象能构造成功。
11. 这个兼容层什么时候能删
不要轻易删。只有满足至少一个条件才考虑:
- 你升级到的新 HTTP 客户端原生支持 TLCP 协议名;
- OkHttp 新版本不再对
session.getProtocol()做封闭枚举; - 你不用 OkHttp/Forest,而是换成原生支持 TLCP 的客户端;
- 你实际接入的是 TLS 1.3 + RFC 8998,不是 TLCP。
删之前要跑:
mvn test
还要做一次真实 TLCP 握手联调。
12. 为什么诊断路径要重新 build SSLContext
业务路径 DdFileUploadClient 可以用配置里的默认值。Web 调试路径不同:页面允许每次请求动态改证书、套件、客户端 KeyStore。
因此 InteractiveUploadService 不能复用单例缓存的 SSLContext,而是:
GmTlcpProperties.Tls tls = request.toTlsProperties();
SSLContext sslContext = sslContextFactory.buildSslContext(tls, request.getTlsProtocol());
这样每次页面提交都按表单参数构造新的上下文,适合联调。
业务路径如果需要动态多租户证书,也应该按类似思路设计,不能把全局单例配置改来改去。
13. 迁移到别的项目时最少要搬哪些东西
如果你要把这套 TLCP 能力搬到另一个 Spring Boot 项目,最少需要:
config/GmTlcpProperties.java
tlcp/KonaProviderInitializer.java
tlcp/TlcpSslContextFactory.java
tlcp/TlcpOkHttpClientFactory.java
tlcp/TlcpProtocolMaskingSslSession.java
tlcp/TlcpProtocolMaskingSslSocket.java
tlcp/TlcpProtocolMaskingSslSocketFactory.java
tlcp/ConfigurableHostnameVerifier.java
如果还想保留诊断能力,再搬:
tlcp/TlcpHandshakeRecorder.java
tlcp/TlcpCertificateDetails.java
如果也需要上传业务示例,再搬:
upload/ContentMeta.java
upload/DdFileUploadClient.java
14. 本篇小结
- Kona 三件套各有职责:Crypto 算法、PKIX 证书、SSL/TLCP 协议。
sm2sig_sm3要在 SSLContext 创建前写入 Kona 系统属性。- TrustManager 用 CA 验证服务端;KeyManager 用客户端证书证明自己。
- Forest 负责 HTTP 请求,但 TLCP 能力要通过自定义 OkHttpClient 注入。
- OkHttp 3.x 有 ConnectionSpec 和 Handshake 协议名两个关卡。
TlcpProtocolMaskingSsl*只改 OkHttp 看到的协议标签,不改变真实 TLCP 握手。
下一篇讲生产/进阶配置:双向认证、密码套件、hostname 校验。

更多推荐




所有评论(0)