【问题标题】:How to configure a Reactive WebClient to use 2-way TLS?如何配置 Reactive WebClient 以使用 2-way TLS?
【发布时间】:2018-11-16 16:11:52
【问题描述】:

我正在尝试将反应式 WebClient 配置为使用 2-way TLS。我使用this answer 作为参考。 (使用 WebClientCustomizer 的那个,而不是使用 InsecureTrustManager 的那个)。

我仔细检查了客户端和服务器端的密钥库和信任库,但服务器返回一个错误,指出客户端没有提供任何证书:

  @Bean
  WebClientCustomizer configureWebclient(@Value("${server.ssl.trust-store}") String trustStorePath, @Value("${server.ssl.trust-store-password}") String trustStorePass,
      @Value("${server.ssl.key-store}") String keyStorePath, @Value("${server.ssl.key-store-password}") String keyStorePass, @Value("${server.ssl.key-alias}") String keyAlias) {

    return new WebClientCustomizer() {

      @Override
      public void customize(Builder webClientBuilder) {
        SslContext sslContext;
        try {
          KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
          trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());

          List<Certificate> certificateCollcetion = Collections.list(trustStore.aliases()).stream().filter(t -> {
            try {
              return trustStore.isCertificateEntry(t);
            } catch (KeyStoreException e1) {
              throw new RuntimeException("Error reading truststore", e1);
            }
          }).map(t -> {
            try {
              return trustStore.getCertificate(t);
            } catch (KeyStoreException e2) {
              throw new RuntimeException("Error reading truststore", e2);
            }
          }).collect(Collectors.toList());

          KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
          keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
          sslContext = SslContextBuilder.forClient()
              .keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))
              .trustManager((X509Certificate[]) certificateCollcetion.toArray(new X509Certificate[certificateCollcetion.size()]))
              .build();
        } catch (Exception e) {
          log.error("Error creating web client", e);
          throw new RuntimeException(e);
        }
        ClientHttpConnector connector = new ReactorClientHttpConnector((opt) -> {
          opt.sslContext(sslContext);
        });
        webClientBuilder.clientConnector(connector);
      }
    };
  }

有人可以分享有关如何正确配置响应式 WebClient 以使用 2-way TLS 的见解吗?

【问题讨论】:

    标签: java spring ssl reactive-programming


    【解决方案1】:

    由于某种原因,当 ssl 上下文这样构建时,服务器不会接受客户端证书:

    sslContext = SslContextBuilder.forClient()
              .keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))
              .trustManager((X509Certificate[]) certificateCollcetion.toArray(new X509Certificate[certificateCollcetion.size()]))
              .build();
    

    为了解决这个问题,我必须初始化一个 KeyManagerFactory:

    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
    keyManagerFactory.init(keyStore, keyStorePass.toCharArray());
    

    然后我用工厂初始化了ssl上下文:

    SslContext sslContext = SslContextBuilder.forClient()
                        .keyManager(keyManagerFactory)
                        .trustManager((X509Certificate[]) certificateCollection.toArray(new X509Certificate[certificateCollection.size()]))
                        .build();
    

    之后,服务器接受了证书,我可以连接了。

    总之,我使用了这个更简洁的解决方案,它利用工厂作为密钥库和信任库:

    @Value("${server.ssl.trust-store}")
    String trustStorePath;
    @Value("${server.ssl.trust-store-password}")
    String trustStorePass;
    @Value("${server.ssl.key-store}")
    String keyStorePath;
    @Value("${server.ssl.key-store-password}")
    String keyStorePass;
    
    @Bean
    public WebClient create2WayTLSWebClient() {
    
        ClientHttpConnector connector = new ReactorClientHttpConnector(
                options -> {
                    options.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
                    options.sslContext(get2WaySSLContext());
                }
        );
    
        return WebClient.builder()
                .clientConnector(connector)
                .build();
    
    }
    
    private SslContext get2WaySSLContext() {
    
        try {
    
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
    
            KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
            keyManagerFactory.init(keyStore, keyStorePass.toCharArray());
    
            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());
    
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
            trustManagerFactory.init(trustStore);
    
            return SslContextBuilder.forClient()
                    .keyManager(keyManagerFactory)
                    .trustManager(trustManagerFactory)
                    .build();
    
        } catch (Exception e) {
            logger.error("Error creating 2-Way TLS WebClient. Check key-store and trust-store.");
            e.printStackTrace();
        }
    
        return null;
    }
    

    请注意,如果您使用的是 Spring 5.1 或更高版本,则此特定实现将不起作用,因为您不能再将 HttpClientOptions 传递给 ReactorClientHttpConnector。使用this link 作为该配置的指南。但是,此答案中的代码内容仍应适用于那种配置。

    【讨论】:

    • 感谢您提供详尽的文档!
    猜你喜欢
    • 2018-06-14
    • 2016-12-01
    • 2020-09-11
    • 2020-05-01
    • 2020-06-03
    • 1970-01-01
    • 2015-09-01
    • 1970-01-01
    • 2020-12-28
    相关资源
    最近更新 更多