【问题标题】:Spring 5 WebClient using ssl使用 ssl 的 Spring 5 WebClient
【发布时间】:2018-01-07 04:21:15
【问题描述】:

我正在尝试查找使用 WebClient 的示例。

我的目标是使用 Spring 5 WebClient 使用 https 和自签名证书查询 REST 服务

有什么例子吗?

【问题讨论】:

标签: spring ssl reactive-programming self-signed spring-webflux


【解决方案1】:

对于那些可能一直坚持如何使用响应式 WebFlux webClient

使用受 https 保护的 REST API 的人

你想创建两个东西

  1. https 启用 REST API - https://github.com/mghutke/HttpsEnabled
  2. 另一个 REST API 作为客户端与 WebClient 使用以上一个 - https://github.com/mghutke/HttpsClient

注意:请通过上述项目查看与上述两个 Spring Boot 应用程序共享的密钥库。并以编程方式添加了 keyManagerFactory 和 TrustManagerFactory。

【讨论】:

    【解决方案2】:

    必须对此进行编辑,以适应 spring-boot 2.0->2.1 的更改。

    另一种方式,如果你想编写生产代码,创建一个这样的 spring bean,它修改注入的 WebClient,使用来自 spring-boot 服务器的设置,用于信任库和密钥库的位置。在客户端,如果您使用的是 2-way-ssl,您只需要提供 Keystore。不确定,为什么 ssl-stuff 没有预先配置且易于注入,类似于非常酷的 spring-boot 服务器设置。

    import io.netty.handler.ssl.SslContext;
    import io.netty.handler.ssl.SslContextBuilder;
    .
    .
    .
    
      @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 (WebClient.Builder webClientBuilder) -> {
              SslContext sslContext;
              final PrivateKey privateKey;
              final X509Certificate[] certificates;
              try {
                final KeyStore trustStore;
                final KeyStore keyStore;
                trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
                trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());
                keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
                List<Certificate> certificateList = 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());
                certificates = certificateList.toArray(new X509Certificate[certificateList.size()]);
                privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray());
                Certificate[] certChain = keyStore.getCertificateChain(keyAlias);
                X509Certificate[] x509CertificateChain = Arrays.stream(certChain)
                    .map(certificate -> (X509Certificate) certificate)
                    .collect(Collectors.toList())
                    .toArray(new X509Certificate[certChain.length]);
                sslContext = SslContextBuilder.forClient()
                    .keyManager(privateKey, keyStorePass, x509CertificateChain)
                    .trustManager(certificates)
                    .build();
      
                HttpClient httpClient = HttpClient.create()
                    .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));
                ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
                webClientBuilder.clientConnector(connector);
              } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException | UnrecoverableKeyException e) {
                throw new RuntimeException(e);
              }
            };
      }
    

    这里是你使用 Webclient 的部分:

    import org.springframework.web.reactive.function.client.WebClient;
    
    @Component
    public class ClientComponent {
    
      public ClientComponent(WebClient.Builder webClientBuilder, @Value("${url}") String url) {
        this.client = webClientBuilder.baseUrl(solrUrl).build();
      }
    }
    

    【讨论】:

    • 当WebClientBuilder被注入到组件中时,会自动使用WebClientCustomizer bean进行自定义?
    • 是的,就是这样。如果我没记错的话,我可以将 WebClient 直接注入到正常应用程序中,但必须从构建器中获取它以进行单元测试。我不知道为什么会这样,但是,您可能必须尝试 - 如果您可以直接获取 WebClient,请随时在此处更正。
    • 我无法让它工作。我通过注入和显式创建 SslContext 进行了尝试,我将其作为选项传递给 ReactorClientHttpConnector,然后我将其传递给构建器以构建 WebClient。但是我打电话的服务器说我没有出示证书。我仔细检查了客户端和服务器端的密钥库和信任库,它们是有效的。此外,可以通过为 2-way TLS 和 SOAP UI 配置的 RestTemplate 访问服务器。
    • 我修复了它,但不确定为什么你的不起作用。我用keystore初始化了一个KeyManagerFactory后把.keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))改成了.keyManager(keyManagerFactory),服务器终于接受了证书。
    • 我可以测试我的旧代码,但问题是,它显然是私钥的证书链和信任链之间的区别,即使它包含相同的密钥。我现在发布的代码已经过测试和工作,但是证书链中密钥的排序可能会破坏信任链(至少我看到了这种情况)。希望现在这对其他人更有帮助。我不知道为什么这个标准用例如此复杂。
    【解决方案3】:

    看起来 Spring 5.1.1 (Spring boot 2.1.0) 从 ReactorClientHttpConnector 中删除了 HttpClientOptions,因此在创建 ReactorClientHttpConnector 的实例时无法配置选项

    现在可行的一个选项是:

    val sslContext = SslContextBuilder
                .forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .build()
    val httpClient = HttpClient.create().secure { t -> t.sslContext(sslContext) }
    val webClient = WebClient.builder().clientConnector(ReactorClientHttpConnector(httpClient)).build()
    

    基本上在创建 HttpClient 时,我们正在配置 insecure sslContext,然后将这个 httpClient 传递给全局ReactorClientHttpConnector 使用。

    另一个选项是为TcpClient配置不安全的sslContext,并使用它来创建HttpClient实例,如下图所示:

    val sslContext = SslContextBuilder
                .forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .build()
    val tcpClient = TcpClient.create().secure { sslProviderBuilder -> sslProviderBuilder.sslContext(sslContext) }
    val httpClient = HttpClient.from(tcpClient)
    val webClient =  WebClient.builder().clientConnector(ReactorClientHttpConnector(httpClient)).build()
    

    更多信息:

    更新:相同代码的 Java 版本

    SslContext context = SslContextBuilder.forClient()
        .trustManager(InsecureTrustManagerFactory.INSTANCE)
        .build();
                    
    HttpClient httpClient = HttpClient.create().secure(t -> t.sslContext(context));
    
    WebClient wc = WebClient
                        .builder()
                        .clientConnector(new ReactorClientHttpConnector(httpClient)).build();
    

    【讨论】:

    • "另一个选项是使用不安全的 sslContext 配置 TcpClient 并使用它来创建 HttpClient 实例"似乎随着 Spring Boot 2.4.0 的发布而被打破(reactor-净值 1.0.1)。您需要添加httpClient = httpClient.secure(tcpClient.configuration().sslProvider()); 以继续使用TcpClient 或使用HttpClientConfig 作为解决方法,直到reactor-netty#1382 的修复程序发布。
    【解决方案4】:

    查看使用 insecure TrustManagerFactory 的示例,它信任所有 X.509 证书(包括自签名证书)而无需任何验证。文档中的重要说明:

    切勿在生产中使用此 TrustManagerFactory。它纯粹是为了测试目的,因此非常不安全。

    @Bean
    public WebClient createWebClient() throws SSLException {
        SslContext sslContext = SslContextBuilder
                .forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .build();
        ClientHttpConnector httpConnector = HttpClient.create().secure(t -> t.sslContext(sslContext) )
        return WebClient.builder().clientConnector(httpConnector).build();
    }
    

    【讨论】:

    • 感谢您的回答,我还需要设置读取和连接超时,如何实现?
    • 有一条评论。最后一行应该是 WebClient.builder().clientConnector(httpConnector).build();否则不会编译。
    • 这个解决方案在将 Spring Boot 升级到 2.1.0 后停止工作,这带来了 Spring 5.1.1,stackoverflow.com/a/53147631/2172731 这对我使用 Spring Security 5.1.1 有效。
    猜你喜欢
    • 1970-01-01
    • 2021-01-19
    • 2020-12-26
    • 1970-01-01
    • 2015-10-04
    • 2018-02-19
    • 2018-04-09
    • 2020-01-31
    • 1970-01-01
    相关资源
    最近更新 更多