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

У меня есть 2 сервиса A и B, которые должны взаимодействовать друг с другом через HTTPS. Я включил TLS, используя свойства server.ssl. * Загрузки Spring для обоих приложений. Я использую WebClient для связи, где служба A вызовет B с помощью веб-клиента, а B отправит ответ обратно A. Для связи через TLS я понимаю, что веб-клиенту потребуются данные хранилища доверенных сертификатов, которые будут иметь сертификаты службы, которая вызывается, то есть Service 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, и, похоже, это работает. Я мог бы позвонить из A в B и ответить обратно.

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

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

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

1 Ответ

0 голосов
/ 09 августа 2020

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

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

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