使用 ssl 的 Spring 5 WebClient

Posted

技术标签:

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

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

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

有什么例子吗?

【问题讨论】:

在 Spring 5.1 中,ReactorClientHttpConnector 不再具有接收选项的构造函数。此处描述了一种可能的解决方案:blog.rohrpostix.net/spring-webflux-webclient-using-ssl 那个页面不见了 【参考方案1】:

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

使用受 https 保护的 REST API 的人

你想创建两个东西

    https 启用 REST API - https://github.com/mghutke/HttpsEnabled 另一个 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()

更多信息:

https://docs.spring.io/spring/docs/5.1.1.RELEASE/spring-framework-reference/web-reactive.html#webflux-client-builder-reactor https://netty.io/4.0/api/io/netty/handler/ssl/util/InsecureTrustManagerFactory.html

更新:相同代码的 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,***.com/a/53147631/2172731 这对我使用 Spring Security 5.1.1 有效。

以上是关于使用 ssl 的 Spring 5 WebClient的主要内容,如果未能解决你的问题,请参考以下文章

Spring-boot 为辅助 SSL 侦听器配置 client-auth=need

Spring boot jpa mysql 连接数据库SSL错误

如何使用带有 SSL 的 Spring WebSocketClient?

spring boot 配置HTTPS

Spring Boot 使用 SSL 连接到 Postgresql

使用 Hibernate、Spring 和 JDBC 配置 SSL 证书