详细分析Android及Java中访问https请求exception(SSLHandshakeException, SSLPeerUnverifiedException)的原因及解决方法。
1、现象
用Android(或Java)测试程序访问下面两个链接。
https链接一:web服务器为jetty,后台语言为java。
https链接二:web服务器为nginx,后台语言为php。
链接一能正常访问,访问链接二报异常,且用HttpURLConnection和apache的HttpClient两种不同的api访问异常信息不同,具体如下:
(1) 用HttpURLConnection访问,测试代码如下:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37  | 
						public static String httpGet(String httpUrl) { 	BufferedReader input = null; 	StringBuilder sb = null; 	URL url = null; 	HttpURLConnection con = null; 	try { 		url = new URL(httpUrl); 		try { 			con = (HttpURLConnection)url.openConnection(); 			input = new BufferedReader(new InputStreamReader(con.getInputStream())); 			sb = new StringBuilder(); 			String s; 			while ((s = input.readLine()) != null) { 				sb.append(s).append("\n"); 			} 		} catch (IOException e) { 			e.printStackTrace(); 		} 	} catch (MalformedURLException e1) { 		e1.printStackTrace(); 	} finally { 		// close buffered 		if (input != null) { 			try { 				input.close(); 			} catch (IOException e) { 				e.printStackTrace(); 			} 		} 		// disconnecting releases the resources held by a connection so they may be closed or reused 		if (con != null) { 			con.disconnect(); 		} 	} 	return sb == null ? null : sb.toString(); }  | 
					
异常信息为:
| 
					 1  | 
						javax.net.ssl.SSLPeerUnverifiedException: No peer certificate  | 
					
(2) 用apache的HttpClient访问,测试代码如下:
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18  | 
						public static String httpGet(String httpUrl) { 	HttpClient httpClient = new HttpClient(); 	GetMethod httpGet = new GetMethod(httpUrl); 	try { 		if (httpClient.executeMethod(httpGet) != HttpStatus.SC_OK) { 			// System.err.println("HttpGet Method failed: " + httpGet.getStatusLine()); 			return null; 		} 		return httpGet.getResponseBodyAsString(); 	} catch (Exception e) { 		e.printStackTrace(); 	} finally { 		httpGet.releaseConnection(); 		httpClient = null; 	} 	return null; }  | 
					
异常信息为:
| 
					 1  | 
						javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target  | 
					
2、原因分析
需要快速寻求答案的可直接看第3部分 解决方式,这部分详细分析原因。
google发现stackoverflow上不少人反应,twitter和新浪微博的api也会报这个异常,不少人反映客户端需要导入证书,其实大可不必,如果要导证书的话,用户不得哭了。。
从上面的情况可以看出,用jetty做为容器是能正常访问的,只是当容器为nginx时才会异常。
配合后台开发调试了很久,开始以为是cipher suite的问题,为此特地把
ssl_ciphers EDH-RSA-DES-CBC3-SHA;
加入了nginx的配置中,后来发现依然无效。stackoverflow发现,如下代码是能正常访问上面异常的https url
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83  | 
						public static String httpGet(String httpUrl) { 	BufferedReader input = null; 	StringBuilder sb = null; 	URL url = null; 	HttpURLConnection con = null; 	try { 		url = new URL(httpUrl); 		try { 			// trust all hosts 			trustAllHosts(); 			HttpsURLConnection https = (HttpsURLConnection)url.openConnection(); 			if (url.getProtocol().toLowerCase().equals("https")) { 				https.setHostnameVerifier(DO_NOT_VERIFY); 				con = https; 			} else { 				con = (HttpURLConnection)url.openConnection(); 			} 			input = new BufferedReader(new InputStreamReader(con.getInputStream())); 			sb = new StringBuilder(); 			String s; 			while ((s = input.readLine()) != null) { 				sb.append(s).append("\n"); 			} 		} catch (IOException e) { 			e.printStackTrace(); 		} 	} catch (MalformedURLException e1) { 		e1.printStackTrace(); 	} finally { 		// close buffered 		if (input != null) { 			try { 				input.close(); 			} catch (IOException e) { 				e.printStackTrace(); 			} 		} 		// disconnecting releases the resources held by a connection so they may be closed or reused 		if (con != null) { 			con.disconnect(); 		} 	} 	return sb == null ? null : sb.toString(); } final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() { 												public boolean verify(String hostname, SSLSession session) { 													return true; 												} 											}; /**  * Trust every server - dont check for any certificate  */ private static void trustAllHosts() { 	final String TAG = "trustAllHosts"; 	// Create a trust manager that does not validate certificate chains 	TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { 		public java.security.cert.X509Certificate[] getAcceptedIssuers() { 			return new java.security.cert.X509Certificate[] {}; 		} 		public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 			Log.i(TAG, "checkClientTrusted"); 		} 		public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 			Log.i(TAG, "checkServerTrusted"); 		} 	} }; 	// Install the all-trusting trust manager 	try { 		SSLContext sc = SSLContext.getInstance("TLS"); 		sc.init(null, trustAllCerts, new java.security.SecureRandom()); 		HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 	} catch (Exception e) { 		e.printStackTrace(); 	} }  | 
					
可以看出其中与之前的HttpsURLConnection测试代码主要的不同就是加入了
| 
					 1  | 
						trustAllHosts();  | 
					
和
| 
					 1  | 
						https.setHostnameVerifier(DO_NOT_VERIFY);  | 
					
表示相信所有证书,并且所有host name验证返回true,这样就能定位到之前的异常是证书验证不通过的问题了。
在上面checkServerTrusted函数中添加断点,查看X509Certificate[] chain的值,即证书信息,发现访问两个不同链接X509Certificate[] chain值有所区别,nginx传过来证书信息缺少了startssl 的ca证书,证书如下:

至此原因大白:
android的证书库里已经带了startssl ca证书,而nginx默认不带startssl ca证书,这样android端访问nginx为容器的https url校验就会失败,jetty默认带startssl ca证书,所以正常。
PS:后来对windows和mac下java访问https也做了测试,发现mac上的jdk缺省不带startssl ca证书所以能访问通过,而加上startssl ca证书后同android一样访问不通过。而windows上的jdk缺省带startssl ca证书同android一样访问失败。
3、解决方式
上面的分析中已经介绍了一种解决方法即客户端相信所有证书,不过这种方式只是规避了问题,同时也给客户端带来了风险,比较合适的解决方式是为nginx添加startssl ca证书,添加方法如下:
First, use the StartSSL™ Control Panel to create a private key and certificate and transfer them to your server. Then execute the following steps (if you use a class 2 certificate replace class1 by class2 in the instructions below):
- Decrypt the private key by using the password you entered when you created your key:
 
openssl rsa -in ssl.key -out /etc/nginx/conf/ssl.key
Alternatively you can also use the Tool Box decryption tool of your StartSSL™ account.
- Protect your key from prying eyes:
 
chmod 600 /etc/nginx/conf/ssl.key
- Fetch the Root CA and Class 1 Intermediate Server CA certificates:
 
wget http://www.startssl.com/certs/ca.pem
wget http://www.startssl.com/certs/sub.class1.server.ca.pem
- Create a unified certificate from your certificate and the CA certificates:
 
cat ssl.crt sub.class1.server.ca.pem ca.pem > /etc/nginx/conf/ssl-unified.crt
- Configure your nginx server to use the new key and certificate (in the global settings or a server section):
 
ssl on;
ssl_certificate /etc/nginx/conf/ssl-unified.crt;
ssl_certificate_key /etc/nginx/conf/ssl.key;
- Tell nginx to reload its configuration:
 
killall -HUP nginx
也可以直接访问install startssl on nginx.
想请问一下大佬们 javax.net .ssl.SSLException: java.security.ProviderException: java.security.KeyException 这个错误该如何解决,在本地调用微信统一下单接口能够获取到返回值,但是放在腾讯云的linux镜像上就不行了,尝试了很多方法都不能解决
厉害了
厉害了
今天刚好用到这里,来补充一个东西。有时候我们证书只有 crt, 但是需要 cre 的证书,这个时候需要中转一下,找了好多资料都没有方案,最后看到一个方法可以成功:
『先将crt证书安装到本机,然后用IE 的导出证书功能就可以导出 cer证书!』
温馨提示,可以使用虚拟机 开 win 哦!
aaaaaaaaaa
。。
lihai