Java应用通过FQDN请求HTTPS时遇CertificateException:无匹配IP的SAN
问题描述
我正尝试从Java应用向precision.epayworldwide.com发送HTTPS请求,使用的代码如下:
URL url = new URL("https://precision.epayworldwide.com/up-interface"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); String responseLine = reader.readLine(); while (responseLine != null) { System.out.printf("%s\n", responseLine); responseLine = reader.readLine(); }
但遇到了如下异常:
Exception in thread "main" org.bouncycastle.tls.TlsFatalAlert: certificate_unknown(46) at org.bouncycastle.jsse.provider.ProvSSLSocketDirect.checkServerTrusted(Unknown Source) at org.bouncycastle.jsse.provider.ProvTlsClient$1.notifyServerCertificate(Unknown Source) at org.bouncycastle.tls.TlsUtils.processServerCertificate(Unknown Source) at org.bouncycastle.tls.TlsClientProtocol.handleServerCertificate(Unknown Source) at org.bouncycastle.tls.TlsClientProtocol.handleHandshakeMessage(Unknown Source) at org.bouncycastle.tls.TlsProtocol.processHandshakeQueue(Unknown Source) at org.bouncycastle.tls.TlsProtocol.processRecord(Unknown Source) at org.bouncycastle.tls.RecordStream.readRecord(Unknown Source) at org.bouncycastle.tls.TlsProtocol.safeReadRecord(Unknown Source) at org.bouncycastle.tls.TlsProtocol.blockForHandshake(Unknown Source) at org.bouncycastle.tls.TlsClientProtocol.connect(Unknown Source) at org.bouncycastle.jsse.provider.ProvSSLSocketDirect.startHandshake(Unknown Source) at org.bouncycastle.jsse.provider.ProvSSLSocketDirect.startHandshake(Unknown Source) at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:523) at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185) at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1296) at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254) at Main.main(Main.java:17) Caused by: java.security.cert.CertificateException: No subject alternative name found matching IP address 195.145.98.203 at org.bouncycastle.jsse.provider.HostnameUtil.checkHostname(Unknown Source) at org.bouncycastle.jsse.provider.ProvX509TrustManager.checkEndpointID(Unknown Source) at org.bouncycastle.jsse.provider.ProvX509TrustManager.checkEndpointID(Unknown Source) at org.bouncycastle.jsse.provider.ProvX509TrustManager.checkExtendedTrust(Unknown Source) at org.bouncycastle.jsse.provider.ProvX509TrustManager.checkTrusted(Unknown Source) at org.bouncycastle.jsse.provider.ProvX509TrustManager.checkServerTrusted(Unknown Source) ... 18 more
我已正确导入该主机名的TLS证书:
Owner: C=GB,L=Billericay,O=Epay Limited,CN=*.epayworldwide.com Issuer: C=US,O=DigiCert Inc,CN=DigiCert TLS RSA SHA256 2020 CA1 Serial number: 97cc7ab5601ca15b1e65530f3a3e8a3 Valid from: Mon Oct 10 02:00:00 CEST 2022 until: Sun Nov 05 00:59:59 CET 2023 Certificate fingerprints: MD5: AF:C4:FA:F6:A2:99:D8:C3:C3:9A:1D:B7:A4:6D:D7:21 SHA1: 4D:55:B9:2B:69:67:2E:AA:2A:B4:3B:B6:D1:BC:35:77:B0:FD:50:A0 SHA256: 96:AF:91:70:80:F6:F1:9E:30:18:CC:97:53:10:B4:7E:B2:CC:37:31:77:CB:C1:E1:1C:14:BC:CF:19:08:04:3B Signature algorithm name: SHA256WITHRSA Version: 3 Trust this certificate? [no]: yes Certificate was added to keystore
我无法理解为何代码会抛出该异常——已搜索到类似问题,但那些用户都是用IP地址而非FQDN连接服务器。我无法控制服务器,也不能修改证书,请问在不关闭主机名验证的前提下,客户端有什么解决办法?
解决办法
这个异常的核心原因是:当前使用的BouncyCastle JSSE Provider在验证证书时,错误地将域名解析出的IP地址当作验证对象,而非你请求的FQDN。以下是几种不关闭主机名验证的可行方案:
方案1:强制使用Java默认JSSE Provider
从异常栈可以看出,当前依赖的是BouncyCastle的SSL实现。你可以在代码中明确指定使用Java内置的JSSE Provider,避免BouncyCastle的异常行为:
// 初始化Java默认SSL上下文 SSLContext sslContext = SSLContext.getInstance("TLS", "SunJSSE"); sslContext.init(null, null, new SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); // 原请求代码保持不变 URL url = new URL("https://precision.epayworldwide.com/up-interface"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); // ... 后续读取响应的代码
方案2:自定义HostnameVerifier适配目标域名
如果必须保留BouncyCastle,可以自定义HostnameVerifier,针对目标域名优先验证FQDN而非IP:
URL url = new URL("https://precision.epayworldwide.com/up-interface"); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); // 设置自定义主机名验证器 conn.setHostnameVerifier(new HostnameVerifier() { @Override public boolean verify(String hostname, SSLSession session) { if ("precision.epayworldwide.com".equals(hostname)) { // 对目标域名使用通配符证书的验证逻辑 return HostnameVerifier.getDefault().verify("*.epayworldwide.com", session); } // 其他域名保持默认验证逻辑 return HostnameVerifier.getDefault().verify(hostname, session); } }); // 后续读取响应的代码保持不变 BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); // ...
方案3:本地hosts文件绑定域名与IP(临时应急)
这是临时测试方案,通过本地hosts文件直接绑定域名和对应IP,避免DNS解析可能带来的异常:
Windows路径:
C:\Windows\System32\drivers\etc\hosts
Linux/macOS路径:/etc/hosts
添加一行:
195.145.98.203 precision.epayworldwide.com
修改后刷新DNS缓存:Windows执行ipconfig /flushdns,Linux/macOS执行sudo dscacheutil -flushcache。
关键说明
- 方案1是最稳妥的,Java默认JSSE实现会严格按照FQDN验证证书,不会错误使用IP作为验证目标。
- 方案2适合必须依赖BouncyCastle的场景,既保留原有依赖又修正了验证逻辑。
- 方案3仅适合测试环境或无法修改代码的临时应急场景,不推荐长期使用。
内容的提问来源于stack exchange,提问作者Ricky Sixx
相关产品推荐
相关产品推荐

