You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 05:24:56