首页 > 解决方案 > 无法从 Spring 应用程序启动多个 HTTPS 连接

问题描述

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

keyStoretrustStore及其密码设置为系统属性时,请求正常工作并收到预期的 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 响应代码,方法是使用返回 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.httpSSLConnectionSocketFactory, HttpClient, HttpGet, HttpResponse但响应代码仍然是 403。

我只能认为 SSL 配置中缺少某些内容,因为系统属性有效。欢迎对我错过的设置SSLContext/SSLSocketFactory或如何解决/更好地调试问题提出任何建议!谢谢!

标签: javaspringsslhttpscertificate

解决方案


I managed to open the HTTPS connections only by using Spring's RestTemplate (org.springframework.web.client.RestTemplate) that uses the org.apache.http.client.HttpClient.

The method for getting the RestTemplate that has in its SSLContext the keyStore, trustStore and their passwords is the following:

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);
}

The way that the RestTemplate is used for the HTTPS call is:

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);

Hope this helps anyone, had a lot of struggle with the HTTPS connections :)


推荐阅读