Java使用PEM文件发送HTTPS请求遇PKIX路径构建失败求助
解决Apache HttpClient SSLHandshakeException: PKIX路径构建失败问题
你的问题出在信任链没有正确加载:curl命令通过--cacert file.pem指定了包含完整信任链的证书文件,但你的Java代码只加载了客户端证书和私钥,没有把PEM里的CA信任证书添加到SSL上下文的信任管理器中,导致SSL握手时无法验证服务端证书的合法性,抛出PKIX路径错误。
下面提供两种解决方案,推荐第二种(更简洁可靠):
方案一:直接使用你生成的PKCS12文件(推荐)
你已经通过OpenSSL生成了包含客户端证书、私钥和信任链的file.p12文件,直接加载这个文件可以避免手动解析PEM的繁琐,代码更简洁:
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.DnsResolver; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.BasicHttpClientConnectionManager; import org.apache.http.impl.conn.SystemDefaultDnsResolver; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyStore; public class Main { public static void main(String[] args) throws Exception { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); // 加载PKCS12文件,里面包含客户端证书、私钥和信任链 KeyStore keyStore = KeyStore.getInstance("PKCS12"); try (InputStream is = Files.newInputStream(Paths.get("file.p12"))) { keyStore.load(is, "password".toCharArray()); } // 初始化KeyManager(用于客户端证书认证) KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); kmf.init(keyStore, "password".toCharArray()); KeyManager[] km = kmf.getKeyManagers(); // 初始化TrustManager(用于验证服务端证书,使用PKCS12里的信任链) TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keyStore); TrustManager[] tm = tmf.getTrustManagers(); // 构建SSL上下文 SSLContext context = SSLContext.getInstance("TLS"); context.init(km, tm, null); // 自定义DNS解析,对应curl的--resolve参数 DnsResolver dnsResolver = new SystemDefaultDnsResolver() { @Override public InetAddress[] resolve(final String host) throws UnknownHostException { if (host.equalsIgnoreCase("destinationHost.com")) { return new InetAddress[]{InetAddress.getByName("100.100.100.100")}; } else { return super.resolve(host); } } }; // 配置连接管理器和Socket工厂 SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(context); BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager( RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", socketFactory) .build(), null, null, dnsResolver ); httpClientBuilder.setConnectionManager(connManager); CloseableHttpClient client = httpClientBuilder.build(); // 执行请求 HttpGet httpGet = new HttpGet("https://destinationHost.com/api"); httpGet.addHeader("Content-Type", "text/plain"); // 对应curl的-H参数 try (CloseableHttpResponse execute = client.execute(httpGet)) { int statusCode = execute.getStatusLine().getStatusCode(); System.out.println("请求状态码:" + statusCode); // 这里可以处理响应内容 } } }
方案二:手动解析PEM文件加载证书和信任链
如果你坚持要直接使用PEM文件,需要修改代码提取所有CA信任证书并添加到信任管理器中:
关键修改点:
- 新增方法提取PEM中的所有证书
- 将除客户端证书外的其他证书加载到信任KeyStore
- 初始化SSLContext时同时传入KeyManager和TrustManager
完整代码:
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.DnsResolver; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.BasicHttpClientConnectionManager; import org.apache.http.impl.conn.SystemDefaultDnsResolver; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.xml.bind.DatatypeConverter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.file.Files; import java.nio.file.Paths; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) throws Exception { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); SSLContext context = SSLContext.getInstance("TLS"); byte[] certAndKey = Files.readAllBytes(Paths.get("file.pem")); // 提取所有证书(客户端证书+信任链CA证书) List<byte[]> allCertBytes = parseAllDERFromPEM(certAndKey, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----"); // 提取私钥 byte[] keyBytes = parseDERFromPEM(certAndKey, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----"); // 第一个证书是客户端证书 X509Certificate clientCert = generateCertificateFromDER(allCertBytes.get(0)); RSAPrivateKey key = generatePrivateKeyFromDER(keyBytes); // 加载客户端证书和私钥到KeyStore(用于客户端认证) KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(null); keyStore.setCertificateEntry("client-cert", clientCert); keyStore.setKeyEntry("client-key", key, "password".toCharArray(), new Certificate[]{clientCert}); // 加载信任链CA证书到TrustStore(用于验证服务端证书) KeyStore trustStore = KeyStore.getInstance("JKS"); trustStore.load(null); for (int i = 1; i < allCertBytes.size(); i++) { X509Certificate caCert = generateCertificateFromDER(allCertBytes.get(i)); trustStore.setCertificateEntry("ca-" + i, caCert); } // 初始化KeyManager和TrustManager KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(keyStore, "password".toCharArray()); KeyManager[] km = kmf.getKeyManagers(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(trustStore); TrustManager[] tm = tmf.getTrustManagers(); // 初始化SSL上下文 context.init(km, tm, null); // 自定义DNS解析(保持不变) DnsResolver dnsResolver = new SystemDefaultDnsResolver() { @Override public InetAddress[] resolve(final String host) throws UnknownHostException { if (host.equalsIgnoreCase("destinationHost.com")) { return new InetAddress[]{InetAddress.getByName("100.100.100.100")}; } else { return super.resolve(host); } } }; SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(context); BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager( RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", socketFactory) .build(), null, null, dnsResolver ); httpClientBuilder.setConnectionManager(connManager); CloseableHttpClient client = httpClientBuilder.build(); HttpGet httpGet = new HttpGet("https://destinationHost.com/api"); httpGet.addHeader("Content-Type", "text/plain"); try (CloseableHttpResponse execute = client.execute(httpGet)) { int statusCode = execute.getStatusLine().getStatusCode(); System.out.println("请求状态码:" + statusCode); assert statusCode == 200; } } // 提取单个DER块 private static byte[] parseDERFromPEM(byte[] pem, String beginDelimiter, String endDelimiter) { String data = new String(pem); String[] tokens = data.split(beginDelimiter); tokens = tokens[1].split(endDelimiter); return DatatypeConverter.parseBase64Binary(tokens[0].trim()); } // 提取所有DER块(支持多个证书) private static List<byte[]> parseAllDERFromPEM(byte[] pem, String beginDelimiter, String endDelimiter) { String data = new String(pem); String[] tokens = data.split(beginDelimiter); List<byte[]> derList = new ArrayList<>(); // 跳过第一个空元素(split后第一个是begin之前的内容) for (int i = 1; i < tokens.length; i++) { String[] parts = tokens[i].split(endDelimiter); if (parts.length > 0) { derList.add(DatatypeConverter.parseBase64Binary(parts[0].trim())); } } return derList; } private static RSAPrivateKey generatePrivateKeyFromDER(byte[] keyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException { PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); java.security.KeyFactory factory = java.security.KeyFactory.getInstance("RSA"); return (RSAPrivateKey) factory.generatePrivate(spec); } private static X509Certificate generateCertificateFromDER(byte[] certBytes) throws CertificateException { CertificateFactory factory = CertificateFactory.getInstance("X.509"); return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(certBytes)); } }
为什么原来的代码会失败?
你之前的代码中,SSLContext.init(km, null, null)的第二个参数传了null,这会让Java使用系统默认的信任库(比如JRE的cacerts文件),而系统默认信任库不包含你PEM文件里的自定义CA证书,所以SSL握手时无法验证服务端证书的路径,导致PKIX错误。
通过自定义TrustManager并加载你的CA信任证书,就能让Java信任服务端的证书,解决握手问题。
内容的提问来源于stack exchange,提问作者ek0
相关产品推荐
相关产品推荐

