某服务商对接公司的在阿里云配置的API网关时,对方开发沟通说我们公司的HTTPS有问题,请求接口后报如下错误:
javax.net.ssl.SSLException: hostname in certificate didn't match: <马赛克.com> != <*.alicloudapi.com> OR <*.alicloudapi.com> OR
看到错误信息,第一反应是HTTPS证书有问题,所以HTTPS握手失败而报错,但是该API网关有多个服务商对结过,均未发生类似的情况。另外,报错信息中的域名alicloudapi.com
显然不是我们公司的域名,而是阿里云的域名。
抱着证书有问题的怀疑,在证书检查平台myssl.com做了个证书检测,结果见下图。
检查报告显示HTTPS的证书一切正常,但是有一点引起了我的注意:检查报告显示该域名有两个证书信息,一个证书是当前域名的,另个域名是似乎是阿里云的域名,该阿里云域名正是报错信息里的域名。
与对方开发进一步沟通后,得知他们使用的JDK版本是1.6版本,于是我也将本地环境切换成JDK1.6后,请求了自己在阿里云配置的多域名证书的HTTPS链接。
import java.net.URL;
import java.net.URLConnection;
public class Application {
public static void main(String[] args) throws Exception {
URL link = new URL("https://img.wakzz.cn/202004/20200407152546.jpg");
URLConnection connection = link.openConnection();
connection.connect();
System.out.println(connection.getContentLength());
}
}
报错信息如下,果然当使用低版本的JDK1.6请求多证书的HTTPS链接后,HTTPS握手失败,但报错信息与接入商的报错信息并不相同。
Exception in thread "main" javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative DNS name matching img.wakzz.cn found.
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1836)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:287)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:281)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1339)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:203)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:848)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:784)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1012)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1320)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1347)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1331)
at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:432)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153)
at Application.main(Application.java:8)
Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching img.wakzz.cn found.
at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:208)
at sun.security.util.HostnameChecker.match(HostnameChecker.java:94)
at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:285)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:271)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1318)
... 11 more
于是进一步,连同HttpClient
依赖的版本也使用了低版本4.2,请求该HTTPS链接后,报错信息终于与接入商的报错信息相同了。
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.2version>
dependency>
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
public class Application {
public static void main(String[] args) throws Exception {
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet("https://img.wakzz.cn/202004/20200407152546.jpg");
HttpResponse response = httpClient.execute(httpGet);
System.out.println(response.getEntity().getContentLength());
}
}
Exception in thread "main" javax.net.ssl.SSLException: hostname in certificate didn't match: != OR OR OR OR OR OR
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:228)
at org.apache.http.conn.ssl.BrowserCompatHostnameVerifier.verify(BrowserCompatHostnameVerifier.java:54)
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:149)
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:130)
at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:572)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:180)
at org.apache.http.impl.conn.ManagedClientConnectionImpl.open(ManagedClientConnectionImpl.java:294)
at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:641)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:480)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:906)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:805)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:784)
at Application.main(Application.java:18)
一个主机可以绑定多个Web服务,比如某个主机的Nginx配置了a.com
、b.com
、c.com
等多个域名的代理。当HTTP请求到达该主机的Nginx时,Nginx可以直接解析Http报文中的Host
值从而将请求转发给特定的Web服务主机。
但是当HTTPS请求到达该主机的Nginx时就出问题了,每个域名绑定一个证书,当HTTPS握手请求到Nginx时,此时HTTPS还没建立完成,请求报文中并没有Host
值,Nginx也就无从得知该给客户端响应哪个域名的证书。
于是在RFC 6066定义了TLS/SSL的SNI扩展server_name
,类似于HTTP请求中的Host
,客户端请求HTTPS握手时的Client Hello
消息中增加一个server_name
字段,值为请求主机的域名。当服务端收到该请求时,解析出server_name
的值从而给客户端响应对应的域名证书。
Enhancements in Java SE 7中显示,SNI扩展是JDK1.7版本才开始支持,因此在上面复现过程中使用JDK1.6时,HTTPS握手失败。
通过抓包HTTPS握手过程,jdk1.8在发送Client Hello
请求时带上了server_name
扩展;
而在jdk1.6在发送Client Hello
请求时并没有server_name
扩展;
而即使使用了高版本的JDK,当使用低版本的HttpClient
请求时,依然会Https握手失败,因为HttpClient
是在4.3.2版本才开始支持SNI扩展功能,见Server Name Indication (SNI) Support。
通过抓包httpclient4.2的Client Hello
请求,显示报文中并没有server_name
扩展,因此当请求多域名证书的域名时,会Https握手失败。
HttpClient
版本升级到4.3.2版本或更高版本;