无法从 Spring 应用程序启动多个 HTTPS 连接

Impossible to start multiple HTTPS connections from Spring application

我有一个 Spring 引导应用程序试图打开 javax.net.ssl.HttpsURLConnection 到服务器,但收到的响应是:java.io.IOException: Server returned HTTP response code: 403 for URL: https://serverIP:8443/path

keyStoretrustStore 及其 passwords 设置为 系统属性请求工作正确并且收到预期的JSON响应:

System.setProperty("javax.net.ssl.keyStore", "src/main/resources/myKeyStore.p12");
System.setProperty("javax.net.ssl.trustStore", "src/main/resources/myTrustStore.truststore");
System.setProperty("javax.net.ssl.keyStorePassword", "myPassword");
System.setProperty("javax.net.ssl.trustStorePassword", "myPassword");

但是当尝试在 SSLContext 中设置信息而不是设置系统属性时收到 403 响应代码,方法是使用 returns 一个 SSLContext对象:

public static SSLContext getSslContext(String trustStoreFile, String keystoreFile, String password)
            throws GeneralSecurityException, IOException {
        final KeyStore keystore = KeyStore.getInstance("pkcs12"); // also tried with JKS

        try (final InputStream inKeystore = new FileInputStream(keystoreFile)) {
            keystore.load(inKeystore, password.toCharArray());
        }

        try (final InputStream inTruststore = new FileInputStream(trustStoreFile)) {
            keystore.load(inTruststore, password.toCharArray());
        }

        final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX"); // also tried with .getDefaultAlgorithm()
        keyManagerFactory.init(keystore, password.toCharArray());

        final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keystore);

        X509TrustManager x509Tm = null;
        for (final TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
            if (trustManager instanceof X509TrustManager) {
                x509Tm = (X509TrustManager) trustManager;
                break;
            }
        }

        final X509TrustManager finalTm = x509Tm;
        final X509ExtendedTrustManager customTm = new X509ExtendedTrustManager() {
            @Override
            public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                return finalTm.getAcceptedIssuers();
            }

            @Override
            public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
            }

            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
            }

            @Override
            public void checkClientTrusted(java.security.cert.X509Certificate[] xcs, String string, Socket socket) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] xcs, String string, Socket socket) throws CertificateException {
            }

            @Override
            public void checkClientTrusted(java.security.cert.X509Certificate[] xcs, String string, SSLEngine ssle) throws CertificateException {
            }

            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] xcs, String string, SSLEngine ssle) throws CertificateException {
            }
        };

        final SSLContext sslContext = SSLContext.getInstance("TLS"); // also tried with SSL
        sslContext.init(
                keyManagerFactory.getKeyManagers(),
                new TrustManager[]{customTm},
                new SecureRandom());

        final HostnameVerifier allHostsValid = new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };

        HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);

        return sslContext;
    }

OBS: trustStore 和keyStore 有相同的密码,这就是为什么该方法只有一个密码参数并且用于密钥和信任管理器工厂。

getSslContext方法的调用和使用方式是:

        final SSLContext sslContext = SSLContextHelper.getSslContext("src/main/resources/myTrustStore.truststore",
                                                                     "src/main/resources/myKeyStore.p12", 
                                                                     "myPassword");
        final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
        final URL url = new URL("https://serverIP:8443/path");
        final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
        urlConnection.setSSLSocketFactory(sslSocketFactory);

        // tried adding some headers to the request
        urlConnection.addRequestProperty("Content-Type", "application/json");
        urlConnection.addRequestProperty("Accept", "application/json");
        urlConnection.addRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:25.0) Gecko/20100101 Firefox/25.0");
        urlConnection.connect();

        final InputStream inputstream = urlConnection.getInputStream();

尝试获取 URL 连接的 inputStream 时在最后一行抛出错误。

此外,我尝试使用 org.apache.http 中的以下 类:SSLConnectionSocketFactory, HttpClient, HttpGet, HttpResponse 但响应代码仍然是 403。

我只能认为 SSL 配置中缺少某些东西,因为系统属性有效。欢迎就我在 SSLContext/SSLSocketFactory 中遗漏的设置或如何 solve/better 调试问题提出任何建议!谢谢!

我只能通过使用 Spring 的 RestTemplate (org.springframework.web.client.RestTemplate) 来打开 HTTPS 连接,它使用 org.apache.http.client.HttpClient

获取 RestTemplate 的方法 SSLContext keyStore、trustStore 及其 passwords 如下:

public RestTemplate getRestTemplate(final String keyStoreFile, final String trustStoreFile,
                                    final String password) throws Exception {

    final SSLContext sslContext = SSLContextBuilder.create()
                                                   .loadKeyMaterial(ResourceUtils.getFile(keyStoreFile), password.toCharArray(), password.toCharArray())
                                                   .loadTrustMaterial(ResourceUtils.getFile(trustStoreFile), password.toCharArray())
                                                   .build();

    final HttpClient client = HttpClients.custom()
                                         .setSSLContext(sslContext)
                                         .build();

    final HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
    httpComponentsClientHttpRequestFactory.setHttpClient(client);

    return new RestTemplate(httpComponentsClientHttpRequestFactory);
}

RestTemplate 用于 HTTPS 调用的方式是:

final String keyStoreFile = "src/main/resources/myKeyStore.p12";
final String trustStoreFile = "src/main/resources/myTrustStore.truststore";
final String password = "myPassword"; // same password for keyStore and trustStore
final String response = getRestTemplate(keyStoreFile, trustStoreFile, password).getForObject("https://serverIP:8443/path", String.class);
LOGGER.info("Response received: " + response);

希望这对任何人都有帮助,因为 HTTPS 连接有很多困难:)