Spring RestTemplate:SSL 握手失败

10

我正在尝试使用基本认证来消费一个restful ws。我没有将任何证书导入我的密钥库中。当我使用Chrome插件Advance Rest client进行测试时(使用base64编码的用户名和密码进行基本认证),我可以看到响应返回。到目前为止一切都很好。

但是,当我开发Java代码来消费此ws时,我遇到了SSL握手失败的问题:

org.springframework.web.client.ResourceAccessException: I/O error: 
Received fatal alert: handshake_failure; nested exception is
javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:453)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:401)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:377)
at test.Rest.main(Rest.java:37) Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
at sun.security.ssl.Alerts.getSSLException(Unknown Source)
at sun.security.ssl.Alerts.getSSLException(Unknown Source)

我的问题是:如果问题是因为我没有将证书导入到密钥库中,那么Java代码和插件都不应该一起工作。但是,在这里,插件可以工作,但我的代码不能。 原因是什么?我的代码有问题吗?
以下是我的代码
RestTemplate restTemplate = new RestTemplate();         
 String plainCreds = "username:pass"; 
 byte[] plainCredsBytes = plainCreds.getBytes(Charset.forName("US-ASCII") );     
 byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes); 
 String base64Creds = new String(base64CredsBytes);

 HttpHeaders headers = new HttpHeaders(); 
 headers.add("Authorization", "Basic " + base64Creds);

 ResponseEntity<String> response =  
          restTemplate.exchange("https://url",HttpMethod.GET,new  
                                  HttpEntity(headers),String.class);

这里是日志文件链接(我已经将服务器名称替换为XXXXXX):http://www.filedropper.com/ssllog 运行以下命令:openssl s_client -showcerts -tls1 -connect host:port
WARNING: can't open config file: /usr/local/ssl/openssl.cnf
CONNECTED(00000164)
8088:error:1408F10B:SSL routines:SSL3_GET_RECORD:wrong version number:.\ssl\s3_pkt.c:362:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 5 bytes and written 0 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1
    Cipher    : 0000
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Key-Arg   : None
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1452011344
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

当我运行命令openssl s_client -connect server:port时,这是输出结果:

WARNING: can't open config file: /usr/local/ssl/openssl.cnf
CONNECTED(00000164)
depth=0 C = US, ST = "XXXXXX", L = XXXXXX, O = XXXXXX, OU = xxxxx, CN = XXXXXXXXX.test.intranet, emailAddress = xxxxx@xxxxx
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C = US, ST = "XXXXXXX ", L = XXXXX, O = XXXXXX, OU = xxxxxx, CN = XXXXXXXXX.test.intranet, emailAddress = xxxxx@xxxxx
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
 0 s:/C=US/ST=XXXXXXX /L=XXXXX/O=XXXXXX/OU=XXXXX/CN=XXXXXX.test.intranet/emailAddress=xxxxx@xxxxx
   i:/DC=intranet/DC=xxxx/CN=XXXXXX DEV Issuing CA
---
Server certificate
-----BEGIN CERTIFICATE-----
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXX.....
-----END CERTIFICATE-----
subject=/C=US/ST=XXXXXXX /L=XXXXX/O=XXXXXX/OU=XXXXX/CN=XXXXXX.test.intranet/emailAddress=xxxxx@xxxxx
issuer=/DC=intranet/DC=XXX/CN=XXXXX DEV Issuing CA
---
No client certificate CA names sent
Peer signing digest: SHA512
Server Temp Key: XXXXX, P-256, 256 bits
---
SSL handshake has read 1895 bytes and written 443 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES128-GCM-SHA256
    Session-ID: 568BF22A5CDBF103155264BBC056B272168AE0777CBC10F055705EB2DD907E5A
    Session-ID-ctx:
    Master-Key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    Key-Arg   : None
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1452012074
    Timeout   : 300 (sec)
    Verify return code: 21 (unable to verify the first certificate)
---
read:errno=0

Chrome和您的应用程序可能使用不同的Java,因此具有不同的设置。即使Java相同,也有无数个原因导致它无法工作。但是,从Chrome开始工作是一个好的开端。启用JVM的SSL调试,例如:添加-Djavax.net.debug=ssl:handshake:verbose并将输出添加到您的问题中,其他调试参数在此处可用http://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html#Debug。 - Michal Foksa
谢谢Michal。我已经添加了运行openssl的输出。请帮帮我。我对ssl和证书方面还很陌生。 - David
我认为您遗漏了-tls1 OpenSSL开关。请使用该选项重新发布输出。 - Michal Foksa
我已经发布了两个输出:一个是使用openssl s_client -showcerts -tls1 -connect host:port,另一个是使用openssl s_client -connect server:port。 - David
奇怪的是,JVM选择使用tls1而不是其他更好的版本。 - Michal Foksa
显示剩余2条评论
2个回答

13

根据javax.net.debug日志,我可以看到您正在使用Java 7并且客户端解析为TLSv1。根据openssl输出,您的服务器不支持TLSv1。

默认情况下,Java 7禁用了TLS ver. 1.1和1.2。

尽管Java SE 7中的SunJSSE支持TLS 1.1和TLS 1.2,但对于客户端连接,默认情况下没有启用任何版本。一些服务器无法正确实现向前兼容性并拒绝与TLS 1.1或TLS 1.2客户端通信。为了实现互操作性,对于客户端连接,SunJSSE不会默认启用TLS 1.1或TLS 1.2。

通过以下方式启用TLSv1.1和TLSv1.2:

  1. JVM参数:

-Dhttps.protocols=TLSv1.2,TLSv1.1,TLSv1
  • 或者从Java代码中设置相同的属性:

    System.setProperty("https.protocols", "TLSv1.2,TLSv1.1,TLSv1");
    
  • 或者安装Java 7的JCE无限强度策略文件。虽然我不确定这一步是否可以完全解决问题,但安装JCE总是值得的,因为它允许JVM使用更强版本的现有算法。

  • 更新于2016年9月29日

    选项1和2中协议的顺序从更好到更差改变了(TLS ver. 1.2至1)。


    1
    感谢Michal的耐心帮助。我们发现托管rest ws的服务器正在使用jdk8,而我的客户端正在使用jdk7。因此,我们将服务器降级为jdk7。我还必须通过使用-Dhttps.protocols = TLSv1,TLSv1.1,TLSv1.2来强制jdk使用TSLv1.2。现在它可以正常工作了。 - David
    1
    在尝试使用-Dhttps.protocols=TLSv1,TLSv1.1,TLSv1.2之前,我还尝试安装了适用于jdk7的JCE无限强度策略。我已经将zip文件中的文件替换了jdk/jre/lib/security中的文件。但是它并没有起作用。 - David
    欢迎您,David,感谢您的反馈和赞赏。 - Michal Foksa
    1
    我会将顺序改为-Dhttps.protocols=TLSv1.2,TLSv1.1,TLSv1,这样它会首先尝试更好的协议,最后退回到v1。 - Bela Vizy

    5

    如果您正在使用Java 7,您需要明确告诉Java使用TLSv1.2协议。以下是使用Spring XML配置的示例。

    ***In any Spring bean (i.e. a controller)***
    
    import org.apache.http.client.HttpClient;
    import org.apache.http.ssl.SSLContexts;
    import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
    import org.apache.http.impl.client.HttpClients;
    import javax.net.ssl.SSLContext;
    
    @Bean(name="client")
    public HttpClient make() throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContexts.custom().build();
        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext,
                new String[]{"TLSv1.2", "TLSv1.1"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        return HttpClients.custom()
                .setSSLSocketFactory(sslConnectionSocketFactory)
                .build();
    }
    
    ***In XML configuration files***
    
    <bean id="factory"
          class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory">
        <property name="httpClient" ref="client"/>
    </bean>
    <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
        <property name="requestFactory" ref="factory"/>
    </bean>
    

    你可���不用XML,使用类似以下的方式实现相同的功能:
    RestTemplate restTemplate;
    
    public HttpClient getHttpClient() throws NoSuchAlgorithmException, KeyManagementException {
        SSLContext sslContext = SSLContexts.custom().build();
        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext,
                new String[]{"TLSv1.2", "TLSv1.1"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        return HttpClients.custom()
                .setSSLSocketFactory(sslConnectionSocketFactory)
                .build();
    }
    
    public void setUp() throws Exception {
        restTemplate = new RestTemplate(
                new HttpComponentsClientHttpRequestFactory(
                        getHttpClient()));
        ...
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接