Spring 5 WebClient使用SSL

54

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

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

有任何示例吗?


9
那个页面已经不存在了。 - J. Doe
6个回答

57

看起来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时,我们配置了不安全的sslContext,并将此httpClient传递给全局使用的ReactorClientHttpConnector。

另一个选项是使用不安全的sslContext配置TcpClient,并使用它来创建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();

1
似乎在Spring Boot 2.4.0 (reactor-netty 1.0.1)版本中,使用"The other option is to configure TcpClient with insecure sslContext and use it to create HttpClient instance"的方法已经失效。你需要添加httpClient = httpClient.secure(tcpClient.configuration().sslProvider());来继续使用TcpClient,或者使用HttpClientConfig作为解决方法,直到reactor-netty#1382问题得到修复。 - Mert Z.

51

查看使用示例不安全的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();
}

谢谢您的回答,我还需要在读取和连接上设置超时,我该如何实现? - Seb
1
有一个注释。最后一行应该是WebClient.builder().clientConnector(httpConnector).build();否则将无法编译。 - Danylo Zatorsky
10
在将Spring Boot升级到2.1.0并引入Spring 5.1.1之后,该解决方案已不再起作用。https://dev59.com/jFcO5IYBdhLWcg3wrTcs#53147631 这个对我来说可行,使用的是Spring Security 5.1.1。 - Munish Chandel

18

为了适应spring-boot 2.0->2.1的变化,必须进行编辑。

如果您想编写生产代码,另一种方法是创建一个spring bean,使用spring-boot服务器的设置修改注入的WebClient,其中包括信任库和密钥库的位置。在客户端中,如果您使用双向ssl,则只需要提供密钥库。不确定为什么ssl设置没有预配置并且不能像真正很酷的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进行定制。 - Skywarp
是的,就是这样。如果我没记错的话,我可以直接在普通应用程序中注入WebClient,但必须从构建器中获取它以进行单元测试。我没有弄清楚为什么会这样,不过你可以尝试一下 - 如果你能直接获取WebClient,请随意在这里更正。 - Frischling
1
我无法让它工作。我尝试了通过注入和显式创建SslContext两种方式,将其作为选项传递给ReactorClientHttpConnector,然后将其传递到构建WebClient的构建器中。但是我调用的服务器说我没有提供证书。我仔细检查了客户端和服务器端的密钥库和信任库,它们都是有效的。此外,该服务器可以通过配置为双向TLS的RestTemplate以及SOAP UI进行访问。 - Skywarp
1
我已经修复了,但不确定为什么你的无法正常工作。在使用密钥库初始化KeyManagerFactory后,我将.keyManager((PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray()))更改为.keyManager(keyManagerFactory),然后服务器终于接受了证书。 - Skywarp
2
我可以测试我的旧代码,问题是私钥的证书链和信任链之间显然存在差异,即使它们包含相同的密钥。我现在发布的代码已经经过测试并且有效,但是证书链中密钥的排序可能会破坏信任链(至少我看到过这种情况发生)。希望现在对其他人更有帮助了。我不知道为什么这个标准用例如此复杂。 - Frischling

4
这个对我有用。使用 ReactorClientHttpConnector,你可以尝试这种方式:Spring framework版本5.3.23(Spring boot版本2.7.4)
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();

希望这个答案对你有所帮助。

2

对于那些可能在如何使用响应式WebFlux webClient 消费 https 保护的 REST API 上遇到困难的人

您需要创建两个东西

  1. 启用https的REST API - https://github.com/mghutke/HttpsEnabled
  2. 另一个REST API作为客户端,使用WebClient来消费上述API - https://github.com/mghutke/HttpsClient

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


0
此外,如果我们需要配置多个SSLContexts,例如,我们有REST API 1和REST API 2,为它们配置了SSLContext sslContext1和SSLContext sslContext2。
问题在于HttpClient.create().secure(...)只允许我们应用一个SSLContext,但在我们的情况下,我们想要多个。
因此,在我们的情况下,解决方案是创建两个不同的WebClient,具有不同的ReactorClientHttpConnector。
// for REST API 1 with sslContext1
WebClient webClient1 = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(
                HttpClient.create()
                        .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext1))
        ))
        .build();

// for REST API 1 with sslContext2
WebClient webClient2 = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(
                HttpClient.create()
                        .secure(sslContextSpec -> sslContextSpec.sslContext(sslContext2))
        ))
        .build();

我们完成了!

另外,需要指出的是,默认情况下这些客户端将共享事件循环组,这是推荐的。但是,如果您使用runOn或使用ReactorResourceFactory进行配置,则不会共享。有关资源的更多信息,请参见此处:https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client-builder-reactor-resources


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接