一、前述
使用httpclient发起https请求时,可能会遇到如下异常:
javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated at sun.security.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:397) at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128) at org.apache.http.conn.ssl.SSLSocketFactory.createSocket(SSLSocketFactory.java:399) at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:143)
网上搜索也能找到一大堆的解决方案,但大部分都类似,就是跳过证书的验证,于是跟着稀里糊涂的将代码拷贝下来使用,结果呢?有的能解决,有的依旧报这个错误。到底咋回事呢,接下来就说说这个问题的解决方案。
二、缘由
首先,要知道导致报这个异常的原因不仅仅是因为证书校验不通过。
都知道,在我们通过https链接服务器时,服务器会给我们返回一个证书,这个证书可能经过CA认证,也可能是未认证的自制证书,客户端拿到这个证书后会对这个证书进行验证,如果是经过CA验证的证书,自然证书校验就能通过,自制证书自然就校验不同过,从而导致上边的异常。
证书校验通过后,还需要校验访问的域名是否和证书指定的域名是否匹配。未匹配也会导致如上异常。
上边两步都校验通过了才开始进行握手,但握手也有可能失败,从而导致上边的异常。
以上三个步骤中任何一个出了问题,都会连接失败。
三、解决方法
通过网上搜索我们大部分都会找到类似如下的解决方案:
SSLContext sslContext; try { sslContext = SSLContext.getInstance("SSL"); // set up a TrustManager that trusts everything try { sslContext.init(null, new TrustManager[] { new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted( X509Certificate[] certs, String authType) { } public void checkServerTrusted( X509Certificate[] certs, String authType) { } } }, new SecureRandom()); } catch (KeyManagementException e) { } SSLSocketFactory ssf = new SSLSocketFactory(sslContext,SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
这个解决方案针对以上的三个步骤中的第一步,就是放弃对证书的校验,但是第二部还可能有问题,要想彻底解决还需要跳过对域名的校验。这里给出最终的解决方案:
try { SSLContext sslContext = SSLContext.getInstance("TLS"); X509TrustManager tm = new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return null; } }; sslContext.init(null,new TrustManager[]{tm},null); sslSocketFactory = new SSLSocketFactory(sslContext,new X509HostnameVerifier(){ @Override public boolean verify(String s, SSLSession sslSession) { return true; } @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 { } }); } catch (GeneralSecurityException e) { log.error("create SSLSocketFactory error:{}",e); } return sslSocketFactory;
这里不仅放弃对证书的校验,也放弃对hostname的校验,通过空实现X509HostnameVerifier类。
以上这个解决方案就会彻底解决问题吗?不一定,还有一个步骤就是握手的步骤也可能出问题,怎么判断是不是握手步骤出了问题呢?可以在代码里做如下设置:
System.setProperty("javax.net.debug","ssl");
做了以上设置后,就可以打印https建立连接的日志了,如下:
true addingastrustedcert: 证书内容略去 triggerseedingofSecureRandom doneseedingSecureRandom executingrequestGETHTTP/1.1 Ignoringunavailableciphersuite:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA Ignoringunavailableciphersuite:TLS_DHE_RSA_WITH_AES_256_CBC_SHA Ignoringunavailableciphersuite:TLS_ECDH_RSA_WITH_AES_256_CBC_SHA Ignoringunsupportedciphersuite:TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 Ignoringunsupportedciphersuite:TLS_DHE_DSS_WITH_AES_256_CBC_SHA256 Ignoringunsupportedciphersuite:TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 Ignoringunsupportedciphersuite:TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 Ignoringunsupportedciphersuite:TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 Ignoringunsupportedciphersuite:TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 Ignoringunsupportedciphersuite:TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 Ignoringunsupportedciphersuite:TLS_RSA_WITH_AES_256_CBC_SHA256 Ignoringunavailableciphersuite:TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA Ignoringunsupportedciphersuite:TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 Ignoringunsupportedciphersuite:TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 Ignoringunavailableciphersuite:TLS_DHE_DSS_WITH_AES_256_CBC_SHA Ignoringunsupportedciphersuite:TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 Ignoringunsupportedciphersuite:TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 Ignoringunsupportedciphersuite:TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 Ignoringunavailableciphersuite:TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA Ignoringunavailableciphersuite:TLS_RSA_WITH_AES_256_CBC_SHA Ignoringunsupportedciphersuite:TLS_RSA_WITH_AES_128_CBC_SHA256 Allowunsaferenegotiation:false Allowlegacyhellomessages:true Isinitialhandshake:true Issecurerenegotiation:false %%Nocachedclientsession ***ClientHello,TLSv1 …… main,WRITE:TLSv1Handshake,length=181 main,READ:TLSv1Alert,length=2 main,RECVTLSv1ALERT:fatal,handshake_failure main,calledcloseSocket() main,handlingexception:javax.net.ssl.SSLHandshakeException:Receivedfatalalert:handshake_failure
通过最后一个词组“handshake_failure”,你一定可以确定是握手失败了。这一般是因为客户端的加密机制太简单,服务器认为不安全,握手失败。
握手失败解决方案就比较简单,下载一个UnlimitedJCEPolicyJDK7.zip 。在http://www.oracle.com/technetwork/java/javase/downloads/index.html下载就好了。里面包含了两个jar。在你的/lib/security,替换后,重新运行看看。
如果以上这些还没能解决,那我也是不知道了,可以留言,我跟你一块找答案^_^。