相信大家都已经被PKIX:unable to find valid certification path to requested target这类问题搞的很烦了,ssl握手咋也不成功,难不成握手的姿势不对?网上也有很多解决办法,大多数的代码都是选择绕过,为啥有些人绕过可以,有些人绕过就不行呢?原因很简单,当服务端选择强制校验客户端证书的时候,你是绕不过去的,你只能选择信任别人,但是你不能让别人都信任你,对不对?所以你构建httpclient客户端时,一定要带上证书,这里以测试环境银联的pfx证书为例。Talk is cheap,show me the code.
第一步,构建KeyStore对象,当然是通过pfx文件来构建啦,例如我这里的pfx文件为:CUPTest1.pfx,path当然就是这个文件所在的绝对路径了,这里补充一点,load方法中的1其实是pfx的证书密码,这个密码不会出现在pfx文件本身中,想想也不可能,这个密码是哪里来的呢,是银联告诉我的,一般pfx证书的说明文档中或说明xml文件中,会有明确的字段告诉你pfx证书的密码是多少。
KeyStore keyStore = getKeyStoreByPfx("D://CUPTest1.pfx","1");
public static KeyStore getKeyStoreByPfx(String path,String password){
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream(new File(path)), password.toCharArray());
return keyStore;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
第二步,选择信任任何人,构建TrustManager对象。这里的代码大家应该看的够多了,意义就一句话,不管是谁我都信了。
TrustManager tm=getTrustManager();
public static TrustManager getTrustManager(){
return 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;
}
};
}
第三步,重点来了,我要敲黑板了!!!很多小伙伴都是因为接下来这一步没有正确初始化KeyManager导致握手失败。补充说明,这里pfx证书的密码1又一次用到了。不知道1是啥的,回去看下第一步。这一步其实是PKIX:unable to find valid certification path to requested target的元凶,不要和我说我把pfx证书转成cer证书在jdk的keystore中安装一下就可以了,我试过了,不行!为了防止有人说我没安装成功,这里上个图:
KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmfactory.init(keyStore,"1".toCharArray());
KeyManager[] keymanagers = kmfactory.getKeyManagers();
第四步,构建SSLContext对象,不管什么代码实现,想要对接https,这个对象是不可或缺的,大家都懂的。当然除了TLSv1之外,还有TLSv1.1等等,这里就不再赘述了。选择一个服务端能兼容的最低版本即可。
SSLContext sslCtx = SSLContext.getInstance("TLSv1");
sslCtx.init(keymanagers, new TrustManager[]{tm}, null);
第五步,构建SSLConnectionSocketFactory对象,这个对象就是构建各种httpclient的最终对象了,直接上代码。补充说明一点,
NoopHostnameVerifier.INSTANCE这玩意儿是啥,这里我解释一下,大家一定在调试过程中遇到这种错:
Certificate for <xx.xx.xx.xx> doesn't match any of the subject alternative names: [chargeback.chinaunionpay.net]
也就是IP和域名不匹配,而NoopHostnameVerifier.INSTANCE就是帮我们客户端跳过这类域名验证的策略。
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslCtx, NoopHostnameVerifier.INSTANCE);
最后一步,构建HttpClient对象就是水到渠成的事情了:
CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
有了这个httpclient,你就可以为所欲为的访问对端的https服务啦!
下面就是实际请求的代码了:
HttpPost httppost = new HttpPost("https://182.180.197.49:8099/internet/BOCMExpSvcProxy");
String content="我是Post请求的内容";
StringEntity se = new StringEntity(content, Charset.forName("UTF-8"));
httppost.setEntity(se);
CloseableHttpResponse response = httpclient.execute(httppost);
HttpEntity entity = response.getEntity();
System.out.println(response.getStatusLine());
System.err.println(EntityUtils.toString(entity));
补充说明一点:
我通过下面的方式来加载客户端证书,实际上测试下来也是不行的,所以KeyManager是必须的!
SSLContexts.custom().loadTrustMaterial(new TrustSelfSignedStrategy()).loadKeyMaterial(keyStore, "1".toCharArray())
.build();
最后补充一点,这个配置挺有用的,可以看到完整的ssl handshake过程,如果你的https请求有问题,加了-Djavax.net.debug=all之后,一定可以在控制台看出错误的端倪。