Включить веб-клиент Spring для связи по HTTPS с другими службами с поддержкой TLS

У меня есть 2 службы A и B, которые должны общаться друг с другом через HTTPS. Я включил TLS, используя свойства server.ssl.* загрузки Spring для обоих приложений. Я использую WebClient для связи, когда служба A будет вызывать B с помощью веб-клиента, а B отправит ответ обратно A. Насколько я понимаю, для связи через TLS веб-клиенту потребуются данные хранилища доверенных сертификатов, которые будут иметь сертификаты службы, которая вызывается, т.е. Служба B.

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;

import org.springframework.boot.web.server.Ssl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;


import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import lombok.extern.slf4j.Slf4j;
import reactor.netty.http.client.HttpClient;


@Slf4j
@Configuration
public class WebClientConfig {

    private final Ssl ssl;

    public WebClientConfig(final Ssl ssl) {
        this.ssl = ssl;
    }

    @Bean
    public WebClient createWebClient() throws Exception {

        if (ssl.isEnabled()) {

            return buildSslEnabledWebClient();

        }

        return WebClient.builder().build();
    }

    private WebClient buildSslEnabledWebClient() throws Exception {

        final String trustStorePath = ssl.getTrustStore();
        final String trustStorePassword = ssl.getTrustStorePassword();
        final KeyStore trustStore = createKeyStore(trustStorePath, trustStorePassword);

        try {

            final TrustManagerFactory trustManager = TrustManagerFactory
                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManager.init(trustStore);

            final SslContext sslContext = SslContextBuilder.forClient().trustManager(trustManager)
                    .build();

            final HttpClient httpClient = HttpClient.create().secure(ssl -> {
                ssl.sslContext(sslContext);
            });

            return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build();

        } catch (NoSuchAlgorithmException | KeyStoreException | SSLException e) {
            log.error("Could not initialize Webclient with the trustore data", e);
            throw e;
        }
    }

    private static KeyStore createKeyStore(final String keyStoreLocation, final String keyStorePassword) {

        try (FileInputStream fis = new FileInputStream(keyStoreLocation)) {

            final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());

            ks.load(fis, keyStorePassword.toCharArray());

            return ks;

        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
            throw new IllegalArgumentException(e);
        }
    }
}

Я создал самоподписанные сертификаты с помощью keytool, и это, похоже, работает. Я мог бы позвонить из А в Б и ответить обратно.

Однако я почему-то чувствую, что настройка не завершена, и поэтому мои вопросы:

  1. Я что-то упускаю?
  2. Это правильный способ сделать это?
  3. Будет ли это работать в производстве с реальными сертификатами?

ОБНОВЛЕНИЕ: исправлен код, понял ошибку после прочтения ответа ниже. Я на самом деле использую обновленный код.


person humbleCoder    schedule 06.08.2020    source источник


Ответы (1)


Мне кажется, что вы правильно загружаете файл хранилища ключей, а также правильно инициализируете trustmanagerfactory с помощью объекта хранилища ключей.

Однако вы не используете trustmanagerfactory для настройки вашего httpclient. Вместо этого вы снова загружаете хранилище ключей из файла, передавая его как объект методу SslContextBuilder.forClient().trustManager. Однако это не сработает, потому что этот метод ожидает файл, содержащий список доверенных сертификатов в формате PEM, в то время как вы передаете хранилище ключей. Javadoc содержит следующую документацию:

public SslContextBuilder trustManager(java.io.File trustCertCollectionFile)

Trusted certificates for verifying the remote endpoint's certificate. The file should contain an X.509 certificate collection in PEM format. null uses the system default.

Если вы загружаете файл в формате PEM, то пример, который вы опубликовали, будет работать. Если вы загружаете файл хранилища ключей, я бы посоветовал следующий фрагмент:

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

final SslContext sslContext = SslContextBuilder.forClient()
    .trustManager(trustManagerFactory)
    .build();

final HttpClient httpClient = HttpClient.create()
    .secure(ssl -> {
        ssl.sslContext(sslContext);
    });
person Hakan54    schedule 09.08.2020
comment
Привет, спасибо, что указали на ошибку. Я обновил код кодом, который я фактически использую, который совпадает с тем, что вы указали. Код, который я вставил, был получен из более ранних проб и ошибок. И да, это работает только для формата PEM. Есть ли другой способ заставить его работать для других форматов? - person humbleCoder; 10.08.2020
comment
Вы можете использовать уже созданный trustmanagerfactory для создания sslcontext. Он имеет несколько перегруженных методов, в которые вы можете передавать различные типы объектов, и один из них — trustmanagerfactory. Пожалуйста, взгляните на второй фрагмент кода. Он вызывает метод trustmanager с объектом trustmanagerfactory. Это должно помочь в вашем случае использования. Пожалуйста, дайте мне сейчас, в какой момент вы застряли - person Hakan54; 10.08.2020