转载自: http://www.trinea.cn/android/android-java-https-ssl-exception-2/
详细分析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):
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.
chmod 600 /etc/nginx/conf/ssl.key
wget http://www.startssl.com/certs/ca.pem
wget http://www.startssl.com/certs/sub.class1.server.ca.pem
cat ssl.crt sub.class1.server.ca.pem ca.pem > /etc/nginx/conf/ssl-unified.crt
ssl on;
ssl_certificate /etc/nginx/conf/ssl-unified.crt;
ssl_certificate_key /etc/nginx/conf/ssl.key;
killall -HUP nginx
也可以直接访问install startssl on nginx.