Skip to content

Commit 6ea2547

Browse files
committed
Add SSL bundle support to WebClient auto-configuration
Introduce `WebClientSsl` interface and auto-configuration to allow a WebClient builder to have custom SSL configuration applied. The previous `ClientHttpConnectorConfiguration` has been been changed to now create `ClientHttpConnectorFactory` instances which can be used directly or by `AutoConfiguredWebClientSsl`. Closes gh-18556
1 parent c59c8cc commit 6ea2547

22 files changed

+893
-65
lines changed

spring-boot-project/spring-boot-autoconfigure/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ dependencies {
5959
exclude group: "commons-logging", module: "commons-logging"
6060
}
6161
optional("org.apache.httpcomponents.client5:httpclient5")
62+
optional("org.apache.httpcomponents.core5:httpcore5-reactive");
6263
optional("org.apache.kafka:kafka-streams")
6364
optional("org.apache.tomcat.embed:tomcat-embed-core")
6465
optional("org.apache.tomcat.embed:tomcat-embed-el")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.reactive.function.client;
18+
19+
import java.util.function.Consumer;
20+
21+
import org.springframework.boot.ssl.SslBundle;
22+
import org.springframework.boot.ssl.SslBundles;
23+
import org.springframework.http.client.reactive.ClientHttpConnector;
24+
import org.springframework.web.reactive.function.client.WebClient;
25+
26+
/**
27+
* An auto-configured {@link WebClientSsl} implementation.
28+
*
29+
* @author Phillip Webb
30+
*/
31+
class AutoConfiguredWebClientSsl implements WebClientSsl {
32+
33+
private final ClientHttpConnectorFactory<?> clientHttpConnectorFactory;
34+
35+
private final SslBundles sslBundles;
36+
37+
AutoConfiguredWebClientSsl(ClientHttpConnectorFactory<?> clientHttpConnectorFactory, SslBundles sslBundles) {
38+
this.clientHttpConnectorFactory = clientHttpConnectorFactory;
39+
this.sslBundles = sslBundles;
40+
}
41+
42+
@Override
43+
public Consumer<WebClient.Builder> fromBundle(String bundleName) {
44+
return fromBundle(this.sslBundles.getBundle(bundleName));
45+
}
46+
47+
@Override
48+
public Consumer<WebClient.Builder> fromBundle(SslBundle bundle) {
49+
return (builder) -> {
50+
ClientHttpConnector connector = this.clientHttpConnectorFactory.createClientHttpConnector(bundle);
51+
builder.clientConnector(connector);
52+
};
53+
}
54+
55+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/function/client/ClientHttpConnectorAutoConfiguration.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors.
2+
* Copyright 2012-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,9 +17,12 @@
1717
package org.springframework.boot.autoconfigure.web.reactive.function.client;
1818

1919
import org.springframework.boot.autoconfigure.AutoConfiguration;
20+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2021
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2122
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2223
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
25+
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
2326
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
2427
import org.springframework.context.annotation.Bean;
2528
import org.springframework.context.annotation.Import;
@@ -36,19 +39,30 @@
3639
* HTTP client library.
3740
*
3841
* @author Brian Clozel
42+
* @author Phillip Webb
3943
* @since 2.1.0
4044
*/
4145
@AutoConfiguration
4246
@ConditionalOnClass(WebClient.class)
43-
@Import({ ClientHttpConnectorConfiguration.ReactorNetty.class, ClientHttpConnectorConfiguration.JettyClient.class,
44-
ClientHttpConnectorConfiguration.HttpClient5.class, ClientHttpConnectorConfiguration.JdkClient.class })
47+
@AutoConfigureAfter(SslAutoConfiguration.class)
48+
@Import({ ClientHttpConnectorFactoryConfiguration.ReactorNetty.class,
49+
ClientHttpConnectorFactoryConfiguration.JettyClient.class,
50+
ClientHttpConnectorFactoryConfiguration.HttpClient5.class,
51+
ClientHttpConnectorFactoryConfiguration.JdkClient.class })
4552
public class ClientHttpConnectorAutoConfiguration {
4653

54+
@Bean
55+
@Lazy
56+
@ConditionalOnMissingBean(ClientHttpConnector.class)
57+
ClientHttpConnector webClientHttpConnector(ClientHttpConnectorFactory<?> clientHttpConnectorFactory) {
58+
return clientHttpConnectorFactory.createClientHttpConnector();
59+
}
60+
4761
@Bean
4862
@Lazy
4963
@Order(0)
5064
@ConditionalOnBean(ClientHttpConnector.class)
51-
public WebClientCustomizer clientConnectorCustomizer(ClientHttpConnector clientHttpConnector) {
65+
public WebClientCustomizer webClientHttpConnectorCustomizer(ClientHttpConnector clientHttpConnector) {
5266
return (builder) -> builder.clientConnector(clientHttpConnector);
5367
}
5468

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.reactive.function.client;
18+
19+
import org.springframework.boot.ssl.SslBundle;
20+
import org.springframework.http.client.reactive.ClientHttpConnector;
21+
22+
/**
23+
* Internal factory used to create {@link ClientHttpConnector} instances.
24+
*
25+
* @param <T> the {@link ClientHttpConnector} type
26+
* @author Phillip Webb
27+
*/
28+
@FunctionalInterface
29+
interface ClientHttpConnectorFactory<T extends ClientHttpConnector> {
30+
31+
default T createClientHttpConnector() {
32+
return createClientHttpConnector(null);
33+
}
34+
35+
T createClientHttpConnector(SslBundle sslBundle);
36+
37+
}
Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@
1818

1919
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
2020
import org.apache.hc.core5.http.nio.AsyncRequestProducer;
21-
import org.eclipse.jetty.client.HttpClient;
22-
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
23-
import org.eclipse.jetty.io.ClientConnector;
24-
import org.eclipse.jetty.util.ssl.SslContextFactory;
21+
import org.apache.hc.core5.reactive.ReactiveResponseConsumer;
2522

2623
import org.springframework.beans.factory.ObjectProvider;
2724
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -30,13 +27,7 @@
3027
import org.springframework.context.annotation.Bean;
3128
import org.springframework.context.annotation.Configuration;
3229
import org.springframework.context.annotation.Import;
33-
import org.springframework.context.annotation.Lazy;
34-
import org.springframework.http.client.reactive.ClientHttpConnector;
35-
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
36-
import org.springframework.http.client.reactive.JdkClientHttpConnector;
37-
import org.springframework.http.client.reactive.JettyClientHttpConnector;
3830
import org.springframework.http.client.reactive.JettyResourceFactory;
39-
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
4031
import org.springframework.http.client.reactive.ReactorResourceFactory;
4132

4233
/**
@@ -47,30 +38,26 @@
4738
*
4839
* @author Brian Clozel
4940
*/
50-
@Configuration(proxyBeanMethods = false)
51-
class ClientHttpConnectorConfiguration {
41+
class ClientHttpConnectorFactoryConfiguration {
5242

5343
@Configuration(proxyBeanMethods = false)
5444
@ConditionalOnClass(reactor.netty.http.client.HttpClient.class)
55-
@ConditionalOnMissingBean(ClientHttpConnector.class)
45+
@ConditionalOnMissingBean(ClientHttpConnectorFactory.class)
5646
@Import(ReactorNettyConfigurations.ReactorResourceFactoryConfiguration.class)
5747
static class ReactorNetty {
5848

5949
@Bean
60-
@Lazy
61-
ReactorClientHttpConnector reactorClientHttpConnector(ReactorResourceFactory reactorResourceFactory,
50+
ReactorClientHttpConnectorFactory reactorClientHttpConnectorFactory(
51+
ReactorResourceFactory reactorResourceFactory,
6252
ObjectProvider<ReactorNettyHttpClientMapper> mapperProvider) {
63-
ReactorNettyHttpClientMapper mapper = mapperProvider.orderedStream()
64-
.reduce((before, after) -> (client) -> after.configure(before.configure(client)))
65-
.orElse((client) -> client);
66-
return new ReactorClientHttpConnector(reactorResourceFactory, mapper::configure);
53+
return new ReactorClientHttpConnectorFactory(reactorResourceFactory, mapperProvider::orderedStream);
6754
}
6855

6956
}
7057

7158
@Configuration(proxyBeanMethods = false)
7259
@ConditionalOnClass(org.eclipse.jetty.reactive.client.ReactiveRequest.class)
73-
@ConditionalOnMissingBean(ClientHttpConnector.class)
60+
@ConditionalOnMissingBean(ClientHttpConnectorFactory.class)
7461
static class JettyClient {
7562

7663
@Bean
@@ -80,40 +67,32 @@ JettyResourceFactory jettyClientResourceFactory() {
8067
}
8168

8269
@Bean
83-
@Lazy
84-
JettyClientHttpConnector jettyClientHttpConnector(JettyResourceFactory jettyResourceFactory) {
85-
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
86-
ClientConnector connector = new ClientConnector();
87-
connector.setSslContextFactory(sslContextFactory);
88-
HttpClientTransportOverHTTP transport = new HttpClientTransportOverHTTP(connector);
89-
HttpClient httpClient = new HttpClient(transport);
90-
return new JettyClientHttpConnector(httpClient, jettyResourceFactory);
70+
JettyClientHttpConnectorFactory jettyClientHttpConnectorFactory(JettyResourceFactory jettyResourceFactory) {
71+
return new JettyClientHttpConnectorFactory(jettyResourceFactory);
9172
}
9273

9374
}
9475

9576
@Configuration(proxyBeanMethods = false)
96-
@ConditionalOnClass({ HttpAsyncClients.class, AsyncRequestProducer.class })
97-
@ConditionalOnMissingBean(ClientHttpConnector.class)
77+
@ConditionalOnClass({ HttpAsyncClients.class, AsyncRequestProducer.class, ReactiveResponseConsumer.class })
78+
@ConditionalOnMissingBean(ClientHttpConnectorFactory.class)
9879
static class HttpClient5 {
9980

10081
@Bean
101-
@Lazy
102-
HttpComponentsClientHttpConnector httpComponentsClientHttpConnector() {
103-
return new HttpComponentsClientHttpConnector();
82+
HttpComponentsClientHttpConnectorFactory httpComponentsClientHttpConnectorFactory() {
83+
return new HttpComponentsClientHttpConnectorFactory();
10484
}
10585

10686
}
10787

10888
@Configuration(proxyBeanMethods = false)
10989
@ConditionalOnClass(java.net.http.HttpClient.class)
110-
@ConditionalOnMissingBean(ClientHttpConnector.class)
90+
@ConditionalOnMissingBean(ClientHttpConnectorFactory.class)
11191
static class JdkClient {
11292

11393
@Bean
114-
@Lazy
115-
JdkClientHttpConnector jdkClientHttpConnector() {
116-
return new JdkClientHttpConnector();
94+
JdkClientHttpConnectorFactory jdkClientHttpConnectorFactory() {
95+
return new JdkClientHttpConnectorFactory();
11796
}
11897

11998
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.reactive.function.client;
18+
19+
import javax.net.ssl.SSLContext;
20+
import javax.net.ssl.SSLEngine;
21+
import javax.net.ssl.SSLException;
22+
23+
import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder;
24+
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
25+
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
26+
import org.apache.hc.client5.http.nio.AsyncClientConnectionManager;
27+
import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
28+
import org.apache.hc.core5.net.NamedEndpoint;
29+
import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier;
30+
import org.apache.hc.core5.reactor.ssl.TlsDetails;
31+
32+
import org.springframework.boot.ssl.SslBundle;
33+
import org.springframework.boot.ssl.SslOptions;
34+
import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector;
35+
36+
/**
37+
* {@link ClientHttpConnectorFactory} for {@link HttpComponentsClientHttpConnector}.
38+
*
39+
* @author Phillip Webb
40+
*/
41+
class HttpComponentsClientHttpConnectorFactory
42+
implements ClientHttpConnectorFactory<HttpComponentsClientHttpConnector> {
43+
44+
@Override
45+
public HttpComponentsClientHttpConnector createClientHttpConnector(SslBundle sslBundle) {
46+
HttpAsyncClientBuilder builder = HttpAsyncClients.custom();
47+
if (sslBundle != null) {
48+
SslOptions options = sslBundle.getOptions();
49+
SSLContext sslContext = sslBundle.createSslContext();
50+
SSLSessionVerifier sessionVerifier = new SSLSessionVerifier() {
51+
52+
@Override
53+
public TlsDetails verify(NamedEndpoint endpoint, SSLEngine sslEngine) throws SSLException {
54+
if (options.getCiphers() != null) {
55+
sslEngine.setEnabledCipherSuites(options.getCiphers().toArray(String[]::new));
56+
}
57+
if (options.getEnabledProtocols() != null) {
58+
sslEngine.setEnabledProtocols(options.getEnabledProtocols().toArray(String[]::new));
59+
}
60+
return null;
61+
}
62+
63+
};
64+
BasicClientTlsStrategy tlsStrategy = new BasicClientTlsStrategy(sslContext, sessionVerifier);
65+
AsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create()
66+
.setTlsStrategy(tlsStrategy)
67+
.build();
68+
builder.setConnectionManager(connectionManager);
69+
}
70+
return new HttpComponentsClientHttpConnector(builder.build());
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.reactive.function.client;
18+
19+
import java.net.http.HttpClient;
20+
import java.net.http.HttpClient.Builder;
21+
import java.util.Set;
22+
23+
import javax.net.ssl.SSLParameters;
24+
25+
import org.springframework.boot.ssl.SslBundle;
26+
import org.springframework.http.client.reactive.JdkClientHttpConnector;
27+
import org.springframework.http.client.reactive.JettyClientHttpConnector;
28+
import org.springframework.util.CollectionUtils;
29+
30+
/**
31+
* {@link ClientHttpConnectorFactory} for {@link JettyClientHttpConnector}.
32+
*
33+
* @author Phillip Webb
34+
*/
35+
class JdkClientHttpConnectorFactory implements ClientHttpConnectorFactory<JdkClientHttpConnector> {
36+
37+
@Override
38+
public JdkClientHttpConnector createClientHttpConnector(SslBundle sslBundle) {
39+
Builder builder = HttpClient.newBuilder();
40+
if (sslBundle != null) {
41+
builder.sslContext(sslBundle.createSslContext());
42+
SSLParameters parameters = new SSLParameters();
43+
parameters.setCipherSuites(asArray(sslBundle.getOptions().getCiphers()));
44+
parameters.setProtocols(asArray(sslBundle.getOptions().getEnabledProtocols()));
45+
builder.sslParameters(parameters);
46+
}
47+
return new JdkClientHttpConnector(builder.build());
48+
}
49+
50+
private String[] asArray(Set<String> set) {
51+
return (CollectionUtils.isEmpty(set)) ? null : set.toArray(String[]::new);
52+
}
53+
54+
}

0 commit comments

Comments
 (0)