今天在开发京东API接口的时候遇到了一个问题。
问题描述:开发京东用户授权,采用https的访问协议获取新的AccessToken时,总是出现异常:
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
之后在网上找了很多例子,最后摘了一个不需要证书的例子:
/** * 向HTTPS地址发送POST请求 * @param reqURL 请求地址 * @param params 请求参数 * @return 响应内容 */ @SuppressWarnings("finally") public static String sendSSLPostRequest(String reqURL, Map<String, String> params){ long responseLength = 0; //响应长度 String responseContent = null; //响应内容 HttpClient httpClient = new DefaultHttpClient(); //创建默认的httpClient实例 X509TrustManager xtm = new X509TrustManager(){ //创建TrustManager public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public X509Certificate[] getAcceptedIssuers() { return null; } }; try { //TLS1.0与SSL3.0基本上没有太大的差别,可粗略理解为TLS是SSL的继承者,但它们使用的是相同的SSLContext SSLContext ctx = SSLContext.getInstance("TLS"); //使用TrustManager来初始化该上下文,TrustManager只是被SSL的Socket所使用 ctx.init(null, new TrustManager[]{xtm}, null); //创建SSLSocketFactory SSLSocketFactory socketFactory = new SSLSocketFactory(ctx); //通过SchemeRegistry将SSLSocketFactory注册到我们的HttpClient上 httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", socketFactory, 443)); HttpPost httpPost = new HttpPost(reqURL); //创建HttpPost List<NameValuePair> formParams = new ArrayList<NameValuePair>(); //构建POST请求的表单参数 for(Map.Entry<String,String> entry : params.entrySet()){ formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } httpPost.setEntity(new UrlEncodedFormEntity(formParams, "UTF-8")); HttpResponse response = httpClient.execute(httpPost); //执行POST请求 HttpEntity entity = response.getEntity(); //获取响应实体 if (null != entity) { responseLength = entity.getContentLength(); responseContent = EntityUtils.toString(entity, "UTF-8"); //EntityUtils.consume(entity); //Consume response content } System.out.println("请求地址: " + httpPost.getURI()); System.out.println("响应状态: " + response.getStatusLine()); System.out.println("响应长度: " + responseLength); System.out.println("响应内容: " + responseContent); } catch (KeyManagementException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { httpClient.getConnectionManager().shutdown(); //关闭连接,释放资源 return responseContent; } }
之后在运行报另一个异常,执行HttpResponse response = httpClient.execute(httpPost); 报的错:
javax.net.ssl.SSLException: hostname in certificate didn't match: <58.83.158.101> != <*.360buy.com>
原因我猜测可能是在验证HOST的时候出现了问题,但我始终传给程序的是域名,为啥就非要解析成ip呢。。。我郁闷。我就一直在想怎么能报HOST变成域名的形式,我debug了httpPost里的HOST没有问题还是域名的,我就想那肯定是httpClient中有问题。最后发现httpClient与socketFactory有关系,而socketFactory就像是一个配置信息的存储,我在这里找到了一个setHostnameVerifier的方法,貌似是验证hostName的,需要参数hostnameVerifier,我像之前那个X509TrustManager一样new了一个X509HostnameVerifier付给setHostnameVerifier方法,然后重写里面的方法,发现了验证的方法verify(String arg0, SSLSession arg1),跳过直接返回true,果然问题解决了。
以下为正确代码:
/** * 发送HTTPS POST请求 * * @param 要访问的HTTPS地址,POST访问的参数Map对象 * @return 返回响应值 * */ public static final String sendHttpsRequestByPost(String url, Map<String, String> params) { String responseContent = null; HttpClient httpClient = new DefaultHttpClient(); //创建TrustManager X509TrustManager xtm = new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public X509Certificate[] getAcceptedIssuers() { return null; } }; //这个好像是HOST验证 X509HostnameVerifier hostnameVerifier = new X509HostnameVerifier() { public boolean verify(String arg0, SSLSession arg1) { return true; } public void verify(String arg0, SSLSocket arg1) throws IOException {} public void verify(String arg0, String[] arg1, String[] arg2) throws SSLException {} public void verify(String arg0, X509Certificate arg1) throws SSLException {} }; try { //TLS1.0与SSL3.0基本上没有太大的差别,可粗略理解为TLS是SSL的继承者,但它们使用的是相同的SSLContext SSLContext ctx = SSLContext.getInstance("TLS"); //使用TrustManager来初始化该上下文,TrustManager只是被SSL的Socket所使用 ctx.init(null, new TrustManager[] { xtm }, null); //创建SSLSocketFactory SSLSocketFactory socketFactory = new SSLSocketFactory(ctx); socketFactory.setHostnameVerifier(hostnameVerifier); //通过SchemeRegistry将SSLSocketFactory注册到我们的HttpClient上 httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", socketFactory, 443)); HttpPost httpPost = new HttpPost(url); List<NameValuePair> formParams = new ArrayList<NameValuePair>(); // 构建POST请求的表单参数 for (Map.Entry<String, String> entry : params.entrySet()) { formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } httpPost.setEntity(new UrlEncodedFormEntity(formParams, "UTF-8")); HttpResponse response = httpClient.execute(httpPost); HttpEntity entity = response.getEntity(); // 获取响应实体 if (entity != null) { responseContent = EntityUtils.toString(entity, "UTF-8"); } } catch (KeyManagementException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (ParseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭连接,释放资源 httpClient.getConnectionManager().shutdown(); } return responseContent; }
为了大家不引错包,我把import也放在这里:
import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.ParseException; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.conn.ssl.X509HostnameVerifier; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils;