Java SSLException:证书中的主机名不匹配

Posted

技术标签:

【中文标题】Java SSLException:证书中的主机名不匹配【英文标题】:Java SSLException: hostname in certificate didn't match 【发布时间】:2011-08-31 12:34:01 【问题描述】:

我一直在使用以下代码连接到谷歌的一项服务。这段代码在我的本地机器上运行良好:

HttpClient client=new DefaultHttpClient();
HttpPost post = new HttpPost("https://www.google.com/accounts/ClientLogin");
post.setEntity(new UrlEncodedFormEntity(myData));
HttpResponse response = client.execute(post);

我把这段代码放在了一个生产环境中,它阻止了 Google.com。根据要求,他们通过允许我访问 IP 来允许与 Google 服务器通信:74.125.236.52 - 这是 Google 的 IP 之一。我也编辑了我的主机文件以添加此条目。

我仍然无法访问 URL,我想知道为什么。所以我把上面的代码换成了:

HttpPost post = new HttpPost("https://74.125.236.52/accounts/ClientLogin");

现在我收到这样的错误:

javax.net.ssl.SSLException:证书中的主机名不匹配: !=

我猜这是因为 Google 有多个 IP。我不能要求网络管理员允许我访问所有这些 IP - 我什至可能无法获得整个列表。

我现在该怎么办? Java级别是否有解决方法?还是完全掌握在网络人手中?

【问题讨论】:

SSL 证书通常带有一个特定的域 name未正确验证。您可以检查您的客户端是否允许您为连接指定显式证书覆盖。 URL 中的主机名必须与证书中的主机名匹配。您应该尝试让它与主机文件一起使用。如果没有,您可以覆盖证书验证例程以也接受 google.com 的 74.125.236.52(不要太宽松!)。 @Thilo : 如何覆盖验证例程? 回复:验证例程:download.oracle.com/javase/1.4.2/docs/api/javax/net/ssl/… 使用 Apache HttpClient 4.4 会变得更糟!它使用 PublicSuffix.org 来验证主机名并拒绝大多数对流行域的请求,例如 *.googleapis.com 【参考方案1】:

您也可以尝试按照here 的描述设置 HostnameVerifier。这对我来说可以避免这个错误。

// Do not do this in production!!!
HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;

DefaultHttpClient client = new DefaultHttpClient();

SchemeRegistry registry = new SchemeRegistry();
SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory();
socketFactory.setHostnameVerifier((X509HostnameVerifier) hostnameVerifier);
registry.register(new Scheme("https", socketFactory, 443));
SingleClientConnManager mgr = new SingleClientConnManager(client.getParams(), registry);
DefaultHttpClient httpClient = new DefaultHttpClient(mgr, client.getParams());

// Set verifier     
HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);

// Example send http request
final String url = "https://encrypted.google.com/";  
HttpPost httpPost = new HttpPost(url);
HttpResponse response = httpClient.execute(httpPost);

【讨论】:

使用这个主机名验证器通常是个坏主意(参见this question)。 Java 1.8 已弃用的资源 为什么要设置两次?【参考方案2】:

证书验证过程将始终验证服务器提供的证书的 DNS 名称,以及客户端使用的 URL 中的服务器主机名。

以下代码

HttpPost post = new HttpPost("https://74.125.236.52/accounts/ClientLogin");

将导致证书验证过程验证服务器颁发的证书的通用名称,即www.google.com是否与主机名匹配,即74.125.236.52。显然,这必然会导致失败(您可以通过浏览器浏览到 URL https://74.125.236.52/accounts/ClientLogin 来验证这一点,并亲自看到产生的错误)。

假设,为了安全起见,您不愿编写自己的TrustManager(除非您了解如何编写安全的,否则您不得这样做),您应该考虑在数据中心建立 DNS 记录以确保对www.google.com 的所有查找都将解析为74.125.236.52;这应该在您的本地 DNS 服务器或操作系统的 hosts 文件中完成;您可能还需要将条目添加到其他域。不用说,您需要确保这与您的 ISP 返回的记录一致。

【讨论】:

按照您的说法,编辑主机文件应该可以解决问题...我仍然不知道还能做什么!顺便说一句,在这种情况下 TrustManager 可以帮助我吗?你能指出我应该学习如何正确使用它的地方吗?谢谢。 @WinOrWin, this site 包含一个 TrustManager 示例。但是,我不建议在生产中使用相同的代码,因为 TrustManager 根本不验证服务器的证书。您应该做的是合并检查以验证74.125.236.52 提供的服务器是否发给www.google.com。当然,这不如纠正 DNS 查找更可取;我建议获取 Wireshark 转储以查看导致 hosts 文件被忽略的问题。 这不是 TrustManager 问题。这是一个 HTTPS HostnameVerifier 问题。 DNS解决方案将解决它。没有 TrustManager 可以解决它。 证书验证过程将始终验证证书的通用名称 [...]”。实际上,这并不正确,并不总是 CN,尤其是在使用 IP 地址时(请参阅this question)。【参考方案3】:

我有类似的问题。我使用的是 android 的 DefaultHttpClient。我读过 HttpsURLConnection 可以处理这种异常。所以我创建了自定义 HostnameVerifier,它使用来自 HttpsURLConnection 的验证器。我还将实现包装到自定义 HttpClient。

public class CustomHttpClient extends DefaultHttpClient 

public CustomHttpClient() 
    super();
    SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory();
    socketFactory.setHostnameVerifier(new CustomHostnameVerifier());
    Scheme scheme = (new Scheme("https", socketFactory, 443));
    getConnectionManager().getSchemeRegistry().register(scheme);

这是 CustomHostnameVerifier 类:

public class CustomHostnameVerifier implements org.apache.http.conn.ssl.X509HostnameVerifier 

@Override
public boolean verify(String host, SSLSession session) 
    HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
    return hv.verify(host, session);


@Override
public void verify(String host, SSLSocket ssl) throws IOException 


@Override
public void verify(String host, X509Certificate cert) throws SSLException 



@Override
public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException 


【讨论】:

拯救了我的一天 :) 谢谢!! 想知道一件事,通过SSL证书是不是一种跳过/? 我们仍在通过 HttpsURLConnection 验证器验证 URl,因此没有绕过 SSL。 这个答案应该被授予诺贝尔奖:) 完美 X509HostnameVerifier 现在已弃用,但可以使用org.apache.http.conn.ssl.NoopHostnameVerifier【参考方案4】:

httpcliet4.3.3中更简洁的方法(仅用于测试环境)如下。

SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();

【讨论】:

这些课程不可用【参考方案5】:

在httpclient-4.3.3.jar中,还有一个HttpClient可以使用:

public static void main (String[] args) throws Exception 
    // org.apache.http.client.HttpClient client = new DefaultHttpClient();
    org.apache.http.client.HttpClient client = HttpClientBuilder.create().build();
    System.out.println("HttpClient = " + client.getClass().toString());
    org.apache.http.client.methods.HttpPost post = new HttpPost("https://www.rideforrainbows.org/");
    org.apache.http.HttpResponse response = client.execute(post);
    java.io.InputStream is = response.getEntity().getContent();
    java.io.BufferedReader rd = new java.io.BufferedReader(new java.io.InputStreamReader(is));
    String line;
    while ((line = rd.readLine()) != null)  
        System.out.println(line);
    

这个 HttpClientBuilder.create().build() 将返回 org.apache.http.impl.client.InternalHttpClient。它可以处理这个证书中的主机名不匹配问题。

【讨论】:

可以通过提供下载 HTTP 库的链接来改进此答案。 使用HttpClientBuilder.create().build() 代替new DefaultHttpClient() 有效。在纯 Java 上,服务器到服务器。谢谢你。赞成。【参考方案6】:

感谢 Vineet Reynolds。您提供的链接包含很多用户 cmets - 我绝望地尝试了其中一个并且它有所帮助。我添加了这个方法:

// Do not do this in production!!!
HttpsURLConnection.setDefaultHostnameVerifier( new HostnameVerifier()
    public boolean verify(String string,SSLSession ssls) 
        return true;
    
);

现在这对我来说似乎很好,尽管我知道这个解决方案是暂时的。我正在与网络人员一起确定我的主机文件被忽略的原因。

【讨论】:

这实际上是个坏主意。顺便说一句,您可能想要验证 /etc/nsswitch.conf 中的查找顺序,并查看查找中是否忽略了 hosts 文件。 好主意,如果你正在做类似... if (url.startsWith("localhost")) disable ssl 请不要将此作为已接受的答案,因为这是一个非常糟糕的主意,无论是否是临时的。在这种情况下,这对您来说可能没问题,但由于类似问题而发现此问题的其他人不太可能一概而论。您最好不要使用 SSL,至少在这种情况下,您不会在安全问题上与任何人开玩笑。可能在您最终并实际解决问题(在与您的网络人员等交谈之后)编辑问题,然后才接受它。 你只是有效地从你家的前门取下了锁,因为你不知道如何使用钥匙。 在我的单元测试中,这对我来说是一个很好的解决方案——wiremock 默认使用自签名证书,上面的代码在我的setUp() 中,以确保在各种构建主机上进行测试期间不会出现任何中断。 【参考方案7】:

担心的是我们不应该使用 ALLOW_ALL_HOSTNAME_VERIFIER。

我如何实现自己的主机名验证器?

class MyHostnameVerifier implements org.apache.http.conn.ssl.X509HostnameVerifier

    @Override
    public boolean verify(String host, SSLSession session) 
        String sslHost = session.getPeerHost();
        System.out.println("Host=" + host);
        System.out.println("SSL Host=" + sslHost);    
        if (host.equals(sslHost)) 
            return true;
         else 
            return false;
        
    

    @Override
    public void verify(String host, SSLSocket ssl) throws IOException 
        String sslHost = ssl.getInetAddress().getHostName();
        System.out.println("Host=" + host);
        System.out.println("SSL Host=" + sslHost);    
        if (host.equals(sslHost)) 
            return;
         else 
            throw new IOException("hostname in certificate didn't match: " + host + " != " + sslHost);
        
    

    @Override
    public void verify(String host, X509Certificate cert) throws SSLException 
        throw new SSLException("Hostname verification 1 not implemented");
    

    @Override
    public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException 
        throw new SSLException("Hostname verification 2 not implemented");
    

让我们测试托管在共享服务器上的https://www.rideforrainbows.org/。

public static void main (String[] args) throws Exception 
    //org.apache.http.conn.ssl.SSLSocketFactory sf = org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory();
    //sf.setHostnameVerifier(new MyHostnameVerifier());
    //org.apache.http.conn.scheme.Scheme sch = new Scheme("https", 443, sf);

    org.apache.http.client.HttpClient client = new DefaultHttpClient();
    //client.getConnectionManager().getSchemeRegistry().register(sch);
    org.apache.http.client.methods.HttpPost post = new HttpPost("https://www.rideforrainbows.org/");
    org.apache.http.HttpResponse response = client.execute(post);
    java.io.InputStream is = response.getEntity().getContent();
    java.io.BufferedReader rd = new java.io.BufferedReader(new java.io.InputStreamReader(is));
    String line;
    while ((line = rd.readLine()) != null)  
        System.out.println(line);
    

SSL 异常:

线程“主”javax.net.ssl.SSLException 中的异常:证书中的主机名不匹配:www.rideforrainbows.org != stac.rt.sg OR stac.rt.sg OR www.stac.rt。新加坡 在 org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:231) ...

使用 MyHostnameVerifier:

public static void main (String[] args) throws Exception 
    org.apache.http.conn.ssl.SSLSocketFactory sf = org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory();
    sf.setHostnameVerifier(new MyHostnameVerifier());
    org.apache.http.conn.scheme.Scheme sch = new Scheme("https", 443, sf);

    org.apache.http.client.HttpClient client = new DefaultHttpClient();
    client.getConnectionManager().getSchemeRegistry().register(sch);
    org.apache.http.client.methods.HttpPost post = new HttpPost("https://www.rideforrainbows.org/");
    org.apache.http.HttpResponse response = client.execute(post);
    java.io.InputStream is = response.getEntity().getContent();
    java.io.BufferedReader rd = new java.io.BufferedReader(new java.io.InputStreamReader(is));
    String line;
    while ((line = rd.readLine()) != null)  
        System.out.println(line);
    

演出:

主持人=www.rideforrainbows.org SSL 主机=www.rideforrainbows.org

至少我有逻辑来比较 (Host == SSL Host) 并返回 true。

以上源代码适用于 httpclient-4.2.3.jar 和 httpclient-4.3.3.jar。

【讨论】:

您的实现只是进行反向 DNS 查找,而不是从证书中检查任何内容。因此它是不安全的。【参考方案8】:

将 java 版本从 1.8.0_40 更新到 1.8.0_181 解决了这个问题。

【讨论】:

【参考方案9】:
            SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
                    SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(),
                    SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

            CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslConnectionSocketFactory).build();

【讨论】:

感谢您发布此问题的答案!在 Stack Overflow 上不鼓励仅使用代码的答案,因为原始发布者(或未来的读者)可能难以理解它们背后的逻辑。请编辑您的答案并包含对您的代码的解释,以便其他人可以从中受益。谢谢!

以上是关于Java SSLException:证书中的主机名不匹配的主要内容,如果未能解决你的问题,请参考以下文章

httpclient信任所有证书解决SSLException:Unrecognized SSL message,plaintext connection

java Java CLI应用程序的代码,用于将主机名/端口的SSL / TLS证书安装到Java的密钥库中

配置Java SSL 访问网站证书

Android HttpClient - 证书中的主机名不匹配 <example.com> != <*.example.com>

使用基于主机名而不是 IP 地址的自签名证书保护 Python WebSocket

Ngrok 主机名 SSL 证书