Add support for JettyClientHttpConnector.

We now support Jetty's reactive HttpClient next to Reactor Netty if jetty-reactive-httpclient is available on the class path.

Closes gh-438.
This commit is contained in:
Mark Paluch
2019-06-28 17:12:36 +02:00
parent 5a47421a03
commit 22ca10c441
8 changed files with 271 additions and 27 deletions

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -105,6 +107,12 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-reactive-httpclient</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>

View File

@@ -17,13 +17,21 @@ package org.springframework.vault.client;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.SslContextBuilder;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.SslConfiguration;
@@ -33,13 +41,39 @@ import static org.springframework.vault.client.ClientHttpRequestFactoryFactory.h
/**
* Factory for {@link ClientHttpConnector} that supports
* {@link ReactorClientHttpConnector}.
* {@link ReactorClientHttpConnector} and {@link JettyClientHttpConnector}.
*
* This factory configures a {@link ClientHttpConnector} depending on the available
* dependencies.
*
* @author Mark Paluch
* @since 2.2
*/
public class ClientHttpConnectorFactory {
private static final boolean REACTOR_NETTY_PRESENT = isPresent("reactor.netty.http.client.HttpClient");
private static final boolean JETTY_PRESENT = isPresent("org.eclipse.jetty.client.HttpClient");
/**
* Checks for presence of all {@code classNames} using this class' classloader.
*
* @param classNames
* @return {@literal true} if all classes are present; {@literal false} if at least
* one class cannot be found.
*/
private static boolean isPresent(String... classNames) {
for (String className : classNames) {
if (!ClassUtils.isPresent(className,
ClientHttpConnectorFactory.class.getClassLoader())) {
return false;
}
}
return true;
}
/**
* Create a {@link ClientHttpConnector} for the given {@link ClientOptions} and
* {@link SslConfiguration}.
@@ -51,23 +85,19 @@ public class ClientHttpConnectorFactory {
public static ClientHttpConnector create(ClientOptions options,
SslConfiguration sslConfiguration) {
HttpClient client = HttpClient.create();
Assert.notNull(options, "ClientOptions must not be null");
Assert.notNull(sslConfiguration, "SslConfiguration must not be null");
if (hasSslConfiguration(sslConfiguration)) {
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
configureSsl(sslConfiguration, sslContextBuilder);
client = client.secure(builder -> {
builder.sslContext(sslContextBuilder);
});
if (REACTOR_NETTY_PRESENT) {
return ReactorNetty.usingReactorNetty(options, sslConfiguration);
}
client = client.tcpConfiguration(it -> it.option(
ChannelOption.CONNECT_TIMEOUT_MILLIS,
Math.toIntExact(options.getConnectionTimeout().toMillis())));
if (JETTY_PRESENT) {
return JettyClient.usingJetty(options, sslConfiguration);
}
return new ReactorClientHttpConnector(client);
throw new IllegalStateException(
"No supported Reactive Http Client library available (Reactor Netty, Jetty)");
}
private static void configureSsl(SslConfiguration sslConfiguration,
@@ -90,4 +120,95 @@ public class ClientHttpConnectorFactory {
throw new IllegalStateException(e);
}
}
/**
* {@link ClientHttpConnector} for Reactor Netty.
*
* @author Mark Paluch
*/
static class ReactorNetty {
static ClientHttpConnector usingReactorNetty(ClientOptions options,
SslConfiguration sslConfiguration) {
HttpClient client = HttpClient.create();
if (hasSslConfiguration(sslConfiguration)) {
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
configureSsl(sslConfiguration, sslContextBuilder);
client = client.secure(builder -> {
builder.sslContext(sslContextBuilder);
});
}
client = client.tcpConfiguration(it -> it.option(
ChannelOption.CONNECT_TIMEOUT_MILLIS,
Math.toIntExact(options.getConnectionTimeout().toMillis())));
return new ReactorClientHttpConnector(client);
}
}
static class JettyClient {
static ClientHttpConnector usingJetty(ClientOptions options,
SslConfiguration sslConfiguration) {
try {
return new JettyClientHttpConnector(configureClient(
getHttpClient(sslConfiguration), options));
}
catch (GeneralSecurityException | IOException e) {
throw new IllegalStateException(e);
}
}
private static org.eclipse.jetty.client.HttpClient configureClient(
org.eclipse.jetty.client.HttpClient httpClient, ClientOptions options) {
httpClient.setConnectTimeout(options.getConnectionTimeout().toMillis());
httpClient.setAddressResolutionTimeout(options.getConnectionTimeout()
.toMillis());
return httpClient;
}
private static org.eclipse.jetty.client.HttpClient getHttpClient(
SslConfiguration sslConfiguration) throws KeyStoreException, IOException,
NoSuchAlgorithmException, CertificateException {
if (hasSslConfiguration(sslConfiguration)) {
SslContextFactory sslContextFactory = new SslContextFactory();
if (sslConfiguration.getKeyStoreConfiguration().isPresent()) {
KeyStore keyStore = ClientHttpRequestFactoryFactory
.getKeyStore(sslConfiguration.getKeyStoreConfiguration());
sslContextFactory.setKeyStore(keyStore);
}
if (sslConfiguration.getTrustStoreConfiguration().isPresent()) {
KeyStore keyStore = ClientHttpRequestFactoryFactory
.getKeyStore(sslConfiguration.getTrustStoreConfiguration());
sslContextFactory.setTrustStore(keyStore);
}
SslConfiguration.KeyConfiguration keyConfiguration = sslConfiguration
.getKeyConfiguration();
if (keyConfiguration.getKeyAlias() != null) {
sslContextFactory.setCertAlias(keyConfiguration.getKeyAlias());
}
if (keyConfiguration.getKeyPassword() != null) {
sslContextFactory.setKeyManagerPassword(new String(keyConfiguration
.getKeyPassword()));
}
return new org.eclipse.jetty.client.HttpClient(sslContextFactory);
}
return new org.eclipse.jetty.client.HttpClient();
}
}
}

View File

@@ -181,11 +181,7 @@ public class ClientHttpRequestFactoryFactory {
KeyStoreConfiguration keyStoreConfiguration, KeyConfiguration keyConfiguration)
throws GeneralSecurityException, IOException {
KeyStore keyStore = KeyStore.getInstance(StringUtils
.hasText(keyStoreConfiguration.getStoreType()) ? keyStoreConfiguration
.getStoreType() : KeyStore.getDefaultType());
loadKeyStore(keyStoreConfiguration, keyStore);
KeyStore keyStore = getKeyStore(keyStoreConfiguration);
KeyManagerFactory keyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
@@ -206,15 +202,23 @@ public class ClientHttpRequestFactoryFactory {
return keyManagerFactory;
}
static KeyStore getKeyStore(KeyStoreConfiguration keyStoreConfiguration)
throws KeyStoreException, IOException, NoSuchAlgorithmException,
CertificateException {
KeyStore keyStore = KeyStore.getInstance(StringUtils
.hasText(keyStoreConfiguration.getStoreType()) ? keyStoreConfiguration
.getStoreType() : KeyStore.getDefaultType());
loadKeyStore(keyStoreConfiguration, keyStore);
return keyStore;
}
static TrustManagerFactory createTrustManagerFactory(
KeyStoreConfiguration keyStoreConfiguration) throws GeneralSecurityException,
IOException {
KeyStore trustStore = KeyStore.getInstance(StringUtils
.hasText(keyStoreConfiguration.getStoreType()) ? keyStoreConfiguration
.getStoreType() : KeyStore.getDefaultType());
loadKeyStore(keyStoreConfiguration, trustStore);
KeyStore trustStore = getKeyStore(keyStoreConfiguration);
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.client;
import org.junit.Test;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.util.Settings;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.springframework.vault.client.ClientHttpConnectorFactory.JettyClient;
import static org.springframework.vault.client.ClientHttpConnectorFactory.ReactorNetty;
/**
* Integration tests for {@link ClientHttpConnectorFactory}.
*
* @author Mark Paluch
*/
public class ClientHttpConnectorFactoryIntegrationTests {
private final String url = new VaultEndpoint().createUriString("sys/health");
@Test
public void reactorNettyClientShouldWork() {
ClientHttpConnector factory = ReactorNetty.usingReactorNetty(new ClientOptions(),
Settings.createSslConfiguration());
WebClient webClient = WebClient.builder().clientConnector(factory).build();
String response = request(webClient);
assertThat(response).isNotNull().contains("initialized");
}
@Test
public void jettyClientShouldWork() {
ClientHttpConnector factory = JettyClient.usingJetty(new ClientOptions(),
Settings.createSslConfiguration());
WebClient webClient = WebClient.builder().clientConnector(factory).build();
String response = request(webClient);
assertThat(response).isNotNull().contains("initialized");
}
private String request(WebClient webClient) {
// Uninitialized and sealed can cause status 500
try {
return webClient.get().uri(url).retrieve().bodyToMono(String.class).block();
}
catch (WebClientResponseException e) {
return e.getResponseBodyAsString();
}
}
}

View File

@@ -59,6 +59,7 @@
<properties>
<httpclient.version>4.5.7</httpclient.version>
<httpcore.version>4.4.11</httpcore.version>
<jetty-client.version>1.0.3</jetty-client.version>
<netty.version>4.1.36.Final</netty.version>
<okhttp3.version>3.14.2</okhttp3.version>
<jackson.version>2.9.9</jackson.version>
@@ -105,6 +106,13 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-reactive-httpclient</artifactId>
<version>${jetty-client.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>

View File

@@ -5,6 +5,7 @@
=== What's new in Spring Vault 2.2
* Support for Key-Value v2 (versioned backend) secrets through `@VaultPropertySource`.
* SpEL support in `@Secret`.
* Add support for Jetty as reactive HttpClient.
[[new-features.2-1-0]]
=== What's new in Spring Vault 2.1

View File

@@ -6,13 +6,18 @@ Spring Vault supports various HTTP clients to access Vault's HTTP API. Spring Va
Dedicated client support originates from <<vault.client-ssl,customized SSL configuration>>
that is scoped only to Spring Vault's client components.
Spring Vault supports following HTTP clients:
Spring Vault supports following HTTP imperative clients:
* Java's builtin `HttpURLConnection` (default client)
* Apache Http Components
* Netty
* OkHttp 3
Spring Vault's reactive integration supports the following reactive HTTP clients:
* Reactor Netty
* Jetty
Using a specific client requires the according dependency to be available on the classpath
so Spring Vault can use the available client for communicating with Vault.
@@ -65,6 +70,28 @@ dependencies to your project. You can omit the version number if using
----
====
.Reactor Netty
====
[source, xml]
----
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
----
====
.Jetty
====
[source, xml]
----
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-reactive-httpclient</artifactId>
</dependency>
----
====
[[vault.client-ssl]]
== Vault Client SSL configuration

View File

@@ -16,7 +16,7 @@ too slow the data repository can also slow down or stop completely until network
== Reactive Vault Client
Spring Vault's reactive client support is built on top of <<vault.authentication.steps,composable authentication steps>> and Spring's functional `WebClient` via Reactor Netty, which features a fully non-blocking, event-driven HTTP client.
Spring Vault's reactive client support is built on top of <<vault.authentication.steps,composable authentication steps>> and Spring's functional `WebClient` via Reactor Netty or Jetty, which feature both a fully non-blocking, event-driven HTTP client.
It exposes `VaultTokenSupplier` as supplier of `VaultToken` to authenticate HTTP requests
and `ReactiveVaultOperations` as the primary entry point. The core configuration of