Java运行时兼容Let's Encrypt DST Root CA X3证书的WebSocket连接问题
问题:WebSocket服务器Java客户端连接失败,浏览器正常
现象
- 浏览器可正常通过WebSocket(wss)连接服务器,能收到101状态码并正常通信
- Java客户端(基于libGDX框架)连接时抛出PKIX路径构建错误
错误信息
执行以下代码时:
WebSocket socket = WebSockets.newSocket(WebSockets.toSecureWebSocketUrl("myDomain", 8080));
抛出错误:
Failed to send an opening handshake request to the server: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
已确认点
- 客户端主机和端口正确:服务器未启动时会抛出“connection refused”错误
- 服务器使用certbot生成的证书,SSL测试评级为A
- 重新签发证书后,仍使用已过期(2021年9月)的DST Root CA X3根证书
需求
- 确认Java报错是否与DST Root CA X3过期有关
- 实现App在运行时(包括Play Store下载后的版本)自动信任服务器的SSL证书,无需因证书更新重新发布App
Java客户端代码(libGDX/Android)
package ...textwebsockets; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.utils.ScreenUtils; import com.github.czyzby.websocket.WebSocket; import com.github.czyzby.websocket.WebSocketListener; import com.github.czyzby.websocket.WebSockets; import java.io.ByteArrayInputStream; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import javax.security.auth.x500.X500Principal; public class TestWebSockets extends ApplicationAdapter { public final String LOG_TAG = "LOG_TestWebSockets"; SpriteBatch batch; Texture img; @Override public void create () { batch = new SpriteBatch(); img = new Texture("badlogic.jpg"); // 使用gdx-websockets库 WebSocket socket = WebSockets.newSocket(WebSockets.toSecureWebSocketUrl("myDomain", 8080)); socket.setSendGracefully(true); socket.addListener(new WebSocketListener() { @Override public boolean onOpen(WebSocket webSocket) { ... } @Override public boolean onClose(WebSocket webSocket, int closeCode, String reason) { ... } @Override public boolean onMessage(WebSocket webSocket, String packet) { ... } @Override public boolean onMessage(WebSocket webSocket, byte[] packet) { ... } @Override public boolean onError(WebSocket webSocket, Throwable error) { Gdx.app.log(LOG_TAG, "onError: " + error.getCause().getMessage() + "; " + error.getMessage()); return false; } }); socket.connect(); } // 省略其他生命周期方法 }
Node.js服务端代码
'use strict'; const express = require('express'); const path = require('path'); const https = require('https'); const fs = require('fs'); const WebSocket = require('ws'); const app = express(); app.use(express.static(path.join(__dirname, '/public'))); const server = https.createServer({ key: fs.readFileSync('./privkey.pem'), cert: fs.readFileSync('./cert.pem') }, app); const wss = new WebSocket.Server({server: server}); wss.on('connection', function (ws) { ... }); server.listen(8080, function () { console.log('Listening on 8080'); }); // 省略其他wss事件处理方法
网页端代码
<script> const wss = new WebSocket('wss://myDomain:8080'); // 省略其他wss事件处理方法 </script>
回答
1. Java报错与DST Root CA X3过期的关系
是的,两者直接相关。Java的默认信任库(cacerts)包含DST Root CA X3,但该根证书2021年9月过期后,旧版Java(尤其是Java 8及更早版本)会拒绝信任由其交叉签名的Let's Encrypt证书。浏览器能正常连接是因为多数浏览器已移除对该过期根证书的依赖,转而信任Let's Encrypt的ISRG Root X1根证书。
2. 实现App自动信任证书且无需更新App的方案
方案一:修复服务器证书链
Certbot生成的fullchain.pem包含服务器证书+中间证书的完整链,服务端代码应使用这个文件而非单独的cert.pem:
const server = https.createServer({ key: fs.readFileSync('./privkey.pem'), cert: fs.readFileSync('./fullchain.pem') // 替换为fullchain.pem }, app);
完整的证书链能帮助旧版Java客户端找到有效的信任路径,即使DST Root CA X3过期,只要客户端信任ISRG Root X1,就能验证通过。
方案二:适配Java/Android信任库
- Android端:将App的编译SDK和目标SDK升级到Android 11+,新版本系统已默认包含ISRG Root X1根证书,会自动信任Let's Encrypt的新证书链。
- Java桌面端:升级到Java 11+,或手动更新Java 8的cacerts信任库,添加ISRG Root X1证书。
方案三:自定义SSL信任管理器(Android端推荐)
通过自定义信任管理器,让App仅信任Let's Encrypt的合法证书链,避免完全禁用SSL验证带来的安全风险:
- 提前将ISRG Root X1根证书嵌入App资源文件
- 自定义信任管理器验证服务器证书链
- 为gdx-websockets配置自定义SSL上下文
示例代码片段:
// 初始化自定义SSL上下文 private SSLContext createSslContext() throws Exception { // 加载嵌入的ISRG Root X1证书 InputStream certInputStream = Gdx.files.internal("isrgrootx1.pem").read(); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate trustedCert = (X509Certificate) cf.generateCertificate(certInputStream); // 创建信任库并添加证书 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setCertificateEntry("isrgrootx1", trustedCert); // 初始化信任管理器 TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keyStore); // 创建SSL上下文 SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); return sslContext; } // 创建WebSocket时配置自定义SSL上下文 WebSocket socket = WebSockets.newSecureSocket(WebSockets.toWebSocketUrl("myDomain", 8080), createSslContext());
内容的提问来源于stack exchange,提问作者user2342558
相关产品推荐
相关产品推荐

