Skip to content

Commit b6befd1

Browse files
scottfrederickphilwebb
authored andcommitted
Add SSL bundle support to RestTemplateBuilder auto-configuration
Update RestTemplateBuilder auto-configuration so that an SSL can be configured via an SSL bundle. Closes gh-34810
1 parent fd5fd14 commit b6befd1

File tree

7 files changed

+141
-22
lines changed

7 files changed

+141
-22
lines changed

spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/rest-client.adoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ The following code shows a typical example:
1313

1414
include::code:MyService[]
1515

16-
TIP: `RestTemplateBuilder` includes a number of useful methods that can be used to quickly configure a `RestTemplate`.
17-
For example, to add BASIC auth support, you can use `builder.basicAuthentication("user", "password").build()`.
16+
`RestTemplateBuilder` includes a number of useful methods that can be used to quickly configure a `RestTemplate`.
17+
For example, to add BASIC authentication support, you can use `builder.basicAuthentication("user", "password").build()`.
18+
To add SSL support using an <<features#features.ssl.bundles,SSL bundle>>, you can use `builder.setSslBundle(sslBundle).build()`.
1819

1920

2021

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929
optional("com.oracle.database.jdbc:ucp")
3030
optional("com.oracle.database.jdbc:ojdbc8")
3131
optional("com.samskivert:jmustache")
32+
optional("com.squareup.okhttp3:okhttp")
3233
optional("com.zaxxer:HikariCP")
3334
optional("io.netty:netty-tcnative-boringssl-static")
3435
optional("io.projectreactor:reactor-tools")

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactories.java

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,30 @@
2323
import java.util.concurrent.TimeUnit;
2424
import java.util.function.Supplier;
2525

26+
import javax.net.ssl.SSLSocketFactory;
27+
import javax.net.ssl.TrustManager;
28+
import javax.net.ssl.X509TrustManager;
29+
30+
import okhttp3.OkHttpClient;
2631
import org.apache.hc.client5.http.classic.HttpClient;
2732
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
2833
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
2934
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
35+
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
36+
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
3037
import org.apache.hc.core5.http.io.SocketConfig;
3138

3239
import org.springframework.boot.context.properties.PropertyMapper;
40+
import org.springframework.boot.ssl.SslBundle;
41+
import org.springframework.boot.ssl.SslOptions;
3342
import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper;
3443
import org.springframework.http.client.ClientHttpRequestFactory;
3544
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
3645
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
3746
import org.springframework.http.client.SimpleClientHttpRequestFactory;
3847
import org.springframework.util.Assert;
3948
import org.springframework.util.ClassUtils;
49+
import org.springframework.util.CollectionUtils;
4050
import org.springframework.util.ReflectionUtils;
4151

4252
/**
@@ -45,6 +55,7 @@
4555
*
4656
* @author Andy Wilkinson
4757
* @author Phillip Webb
58+
* @author Scott Frederick
4859
* @since 3.0.0
4960
*/
5061
public final class ClientHttpRequestFactories {
@@ -134,25 +145,39 @@ private static <T extends ClientHttpRequestFactory> T createRequestFactory(Class
134145
static class HttpComponents {
135146

136147
static HttpComponentsClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) {
137-
HttpComponentsClientHttpRequestFactory requestFactory = createRequestFactory(settings.readTimeout());
148+
HttpComponentsClientHttpRequestFactory requestFactory = createRequestFactory(settings.readTimeout(),
149+
settings.sslBundle());
138150
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
139151
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
140152
map.from(settings::bufferRequestBody).to(requestFactory::setBufferRequestBody);
141153
return requestFactory;
142154
}
143155

144-
private static HttpComponentsClientHttpRequestFactory createRequestFactory(Duration readTimeout) {
145-
return (readTimeout != null) ? new HttpComponentsClientHttpRequestFactory(createHttpClient(readTimeout))
146-
: new HttpComponentsClientHttpRequestFactory();
156+
private static HttpComponentsClientHttpRequestFactory createRequestFactory(Duration readTimeout,
157+
SslBundle sslBundle) {
158+
return new HttpComponentsClientHttpRequestFactory(createHttpClient(readTimeout, sslBundle));
147159
}
148160

149-
private static HttpClient createHttpClient(Duration readTimeout) {
150-
SocketConfig socketConfig = SocketConfig.custom()
151-
.setSoTimeout((int) readTimeout.toMillis(), TimeUnit.MILLISECONDS)
152-
.build();
153-
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
154-
.setDefaultSocketConfig(socketConfig)
155-
.build();
161+
private static HttpClient createHttpClient(Duration readTimeout, SslBundle sslBundle) {
162+
PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder
163+
.create();
164+
if (readTimeout != null) {
165+
SocketConfig socketConfig = SocketConfig.custom()
166+
.setSoTimeout((int) readTimeout.toMillis(), TimeUnit.MILLISECONDS)
167+
.build();
168+
connectionManagerBuilder.setDefaultSocketConfig(socketConfig);
169+
}
170+
if (sslBundle != null) {
171+
SslOptions options = sslBundle.getOptions();
172+
String[] enabledProtocols = (!CollectionUtils.isEmpty(options.getEnabledProtocols()))
173+
? options.getEnabledProtocols().toArray(String[]::new) : null;
174+
String[] ciphers = (!CollectionUtils.isEmpty(options.getCiphers()))
175+
? options.getCiphers().toArray(String[]::new) : null;
176+
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslBundle.createSslContext(),
177+
enabledProtocols, ciphers, new DefaultHostnameVerifier());
178+
connectionManagerBuilder.setSSLSocketFactory(socketFactory);
179+
}
180+
PoolingHttpClientConnectionManager connectionManager = connectionManagerBuilder.build();
156181
return HttpClientBuilder.create().setConnectionManager(connectionManager).build();
157182
}
158183

@@ -166,13 +191,27 @@ static class OkHttp {
166191
static OkHttp3ClientHttpRequestFactory get(ClientHttpRequestFactorySettings settings) {
167192
Assert.state(settings.bufferRequestBody() == null,
168193
() -> "OkHttp3ClientHttpRequestFactory does not support request body buffering");
169-
OkHttp3ClientHttpRequestFactory requestFactory = new OkHttp3ClientHttpRequestFactory();
194+
OkHttp3ClientHttpRequestFactory requestFactory = createRequestFactory(settings.sslBundle());
170195
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
171196
map.from(settings::connectTimeout).asInt(Duration::toMillis).to(requestFactory::setConnectTimeout);
172197
map.from(settings::readTimeout).asInt(Duration::toMillis).to(requestFactory::setReadTimeout);
173198
return requestFactory;
174199
}
175200

201+
private static OkHttp3ClientHttpRequestFactory createRequestFactory(SslBundle sslBundle) {
202+
if (sslBundle != null) {
203+
SSLSocketFactory socketFactory = sslBundle.createSslContext().getSocketFactory();
204+
TrustManager[] trustManagers = sslBundle.getManagers().getTrustManagers();
205+
Assert.state(trustManagers.length == 1,
206+
"Trust material must be provided in the SSL bundle for OkHttp3ClientHttpRequestFactory");
207+
OkHttpClient client = new OkHttpClient.Builder()
208+
.sslSocketFactory(socketFactory, (X509TrustManager) trustManagers[0])
209+
.build();
210+
return new OkHttp3ClientHttpRequestFactory(client);
211+
}
212+
return new OkHttp3ClientHttpRequestFactory();
213+
}
214+
176215
}
177216

178217
/**

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettings.java

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.time.Duration;
2020

21+
import org.springframework.boot.ssl.SslBundle;
2122
import org.springframework.http.client.ClientHttpRequestFactory;
2223

2324
/**
@@ -26,20 +27,37 @@
2627
* @param connectTimeout the connect timeout
2728
* @param readTimeout the read timeout
2829
* @param bufferRequestBody if request body buffering is used
30+
* @param sslBundle the SSL bundle providing SSL configuration
2931
* @author Andy Wilkinson
3032
* @author Phillip Webb
33+
* @author Scott Frederick
3134
* @since 3.0.0
3235
* @see ClientHttpRequestFactories
3336
*/
34-
public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout,
35-
Boolean bufferRequestBody) {
37+
public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, Boolean bufferRequestBody,
38+
SslBundle sslBundle) {
3639

3740
/**
3841
* Use defaults for the {@link ClientHttpRequestFactory} which can differ depending on
3942
* the implementation.
4043
*/
4144
public static final ClientHttpRequestFactorySettings DEFAULTS = new ClientHttpRequestFactorySettings(null, null,
42-
null);
45+
null, null);
46+
47+
/**
48+
* Create a new {@link ClientHttpRequestFactorySettings} instance.
49+
* @param connectTimeout the connection timeout
50+
* @param readTimeout the read timeout
51+
* @param bufferRequestBody the bugger request body
52+
* @param sslBundle the ssl bundle
53+
* @since 3.1.0
54+
*/
55+
public ClientHttpRequestFactorySettings {
56+
}
57+
58+
public ClientHttpRequestFactorySettings(Duration connectTimeout, Duration readTimeout, Boolean bufferRequestBody) {
59+
this(connectTimeout, readTimeout, bufferRequestBody, null);
60+
}
4361

4462
/**
4563
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated
@@ -48,7 +66,8 @@ public record ClientHttpRequestFactorySettings(Duration connectTimeout, Duration
4866
* @return a new {@link ClientHttpRequestFactorySettings} instance
4967
*/
5068
public ClientHttpRequestFactorySettings withConnectTimeout(Duration connectTimeout) {
51-
return new ClientHttpRequestFactorySettings(connectTimeout, this.readTimeout, this.bufferRequestBody);
69+
return new ClientHttpRequestFactorySettings(connectTimeout, this.readTimeout, this.bufferRequestBody,
70+
this.sslBundle);
5271
}
5372

5473
/**
@@ -59,7 +78,8 @@ public ClientHttpRequestFactorySettings withConnectTimeout(Duration connectTimeo
5978
*/
6079

6180
public ClientHttpRequestFactorySettings withReadTimeout(Duration readTimeout) {
62-
return new ClientHttpRequestFactorySettings(this.connectTimeout, readTimeout, this.bufferRequestBody);
81+
return new ClientHttpRequestFactorySettings(this.connectTimeout, readTimeout, this.bufferRequestBody,
82+
this.sslBundle);
6383
}
6484

6585
/**
@@ -69,7 +89,20 @@ public ClientHttpRequestFactorySettings withReadTimeout(Duration readTimeout) {
6989
* @return a new {@link ClientHttpRequestFactorySettings} instance
7090
*/
7191
public ClientHttpRequestFactorySettings withBufferRequestBody(Boolean bufferRequestBody) {
72-
return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, bufferRequestBody);
92+
return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, bufferRequestBody,
93+
this.sslBundle);
94+
}
95+
96+
/**
97+
* Return a new {@link ClientHttpRequestFactorySettings} instance with an updated SSL
98+
* bundle setting.
99+
* @param sslBundle the new SSL bundle setting
100+
* @return a new {@link ClientHttpRequestFactorySettings} instance
101+
* @since 3.1.0
102+
*/
103+
public ClientHttpRequestFactorySettings withSslBundle(SslBundle sslBundle) {
104+
return new ClientHttpRequestFactorySettings(this.connectTimeout, this.readTimeout, this.bufferRequestBody,
105+
sslBundle);
73106
}
74107

75108
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/client/RestTemplateBuilder.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import reactor.netty.http.client.HttpClientRequest;
3434

3535
import org.springframework.beans.BeanUtils;
36+
import org.springframework.boot.ssl.SslBundle;
3637
import org.springframework.http.client.ClientHttpRequest;
3738
import org.springframework.http.client.ClientHttpRequestFactory;
3839
import org.springframework.http.client.ClientHttpRequestInterceptor;
@@ -64,6 +65,7 @@
6465
* @author Dmytro Nosan
6566
* @author Kevin Strijbos
6667
* @author Ilya Lukyanovich
68+
* @author Scott Frederick
6769
* @since 1.4.0
6870
*/
6971
public class RestTemplateBuilder {
@@ -453,6 +455,19 @@ public RestTemplateBuilder setBufferRequestBody(boolean bufferRequestBody) {
453455
this.customizers, this.requestCustomizers);
454456
}
455457

458+
/**
459+
* Sets the SSL bundle on the underlying {@link ClientHttpRequestFactory}.
460+
* @param sslBundle the SSL bundle
461+
* @return a new builder instance
462+
* @since 2.1.0
463+
*/
464+
public RestTemplateBuilder setSslBundle(SslBundle sslBundle) {
465+
return new RestTemplateBuilder(this.requestFactorySettings.withSslBundle(sslBundle), this.detectRequestFactory,
466+
this.rootUri, this.messageConverters, this.interceptors, this.requestFactory, this.uriTemplateHandler,
467+
this.errorHandler, this.basicAuthentication, this.defaultHeaders, this.customizers,
468+
this.requestCustomizers);
469+
}
470+
456471
/**
457472
* Set the {@link RestTemplateCustomizer RestTemplateCustomizers} that should be
458473
* applied to the {@link RestTemplate}. Customizers are applied in the order that they

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/webservices/client/HttpWebServiceMessageSenderBuilder.java

Lines changed: 15 additions & 2 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.
@@ -20,6 +20,7 @@
2020
import java.util.function.Function;
2121
import java.util.function.Supplier;
2222

23+
import org.springframework.boot.ssl.SslBundle;
2324
import org.springframework.boot.web.client.ClientHttpRequestFactories;
2425
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
2526
import org.springframework.http.client.ClientHttpRequestFactory;
@@ -40,6 +41,8 @@ public class HttpWebServiceMessageSenderBuilder {
4041

4142
private Duration readTimeout;
4243

44+
private SslBundle sslBundle;
45+
4346
private Function<ClientHttpRequestFactorySettings, ClientHttpRequestFactory> requestFactory;
4447

4548
/**
@@ -62,6 +65,16 @@ public HttpWebServiceMessageSenderBuilder setReadTimeout(Duration readTimeout) {
6265
return this;
6366
}
6467

68+
/**
69+
* Set an {@link SslBundle} that will be used to configure a secure connection.
70+
* @param sslBundle the SSL bundle
71+
* @return a new builder instance
72+
*/
73+
public HttpWebServiceMessageSenderBuilder sslBundle(SslBundle sslBundle) {
74+
this.sslBundle = sslBundle;
75+
return this;
76+
}
77+
6578
/**
6679
* Set the {@code Supplier} of {@link ClientHttpRequestFactory} that should be called
6780
* to create the HTTP-based {@link WebServiceMessageSender}.
@@ -100,7 +113,7 @@ public WebServiceMessageSender build() {
100113

101114
private ClientHttpRequestFactory getRequestFactory() {
102115
ClientHttpRequestFactorySettings settings = new ClientHttpRequestFactorySettings(this.connectTimeout,
103-
this.readTimeout, null);
116+
this.readTimeout, null, this.sslBundle);
104117
return (this.requestFactory != null) ? this.requestFactory.apply(settings)
105118
: ClientHttpRequestFactories.get(settings);
106119
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/client/ClientHttpRequestFactorySettingsTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020

2121
import org.junit.jupiter.api.Test;
2222

23+
import org.springframework.boot.ssl.SslBundle;
24+
2325
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.mockito.Mockito.mock;
2427

2528
/**
2629
* Tests for {@link ClientHttpRequestFactorySettings}.
@@ -37,6 +40,7 @@ void defaultsHasNullValues() {
3740
assertThat(settings.connectTimeout()).isNull();
3841
assertThat(settings.readTimeout()).isNull();
3942
assertThat(settings.bufferRequestBody()).isNull();
43+
assertThat(settings.sslBundle()).isNull();
4044
}
4145

4246
@Test
@@ -46,6 +50,7 @@ void withConnectTimeoutReturnsInstanceWithUpdatedConnectionTimeout() {
4650
assertThat(settings.connectTimeout()).isEqualTo(ONE_SECOND);
4751
assertThat(settings.readTimeout()).isNull();
4852
assertThat(settings.bufferRequestBody()).isNull();
53+
assertThat(settings.sslBundle()).isNull();
4954
}
5055

5156
@Test
@@ -55,6 +60,7 @@ void withReadTimeoutReturnsInstanceWithUpdatedReadTimeout() {
5560
assertThat(settings.connectTimeout()).isNull();
5661
assertThat(settings.readTimeout()).isEqualTo(ONE_SECOND);
5762
assertThat(settings.bufferRequestBody()).isNull();
63+
assertThat(settings.sslBundle()).isNull();
5864
}
5965

6066
@Test
@@ -64,6 +70,17 @@ void withBufferRequestBodyReturnsInstanceWithUpdatedBufferRequestBody() {
6470
assertThat(settings.connectTimeout()).isNull();
6571
assertThat(settings.readTimeout()).isNull();
6672
assertThat(settings.bufferRequestBody()).isTrue();
73+
assertThat(settings.sslBundle()).isNull();
74+
}
75+
76+
@Test
77+
void withSslBundleReturnsInstanceWithUpdatedSslBundle() {
78+
SslBundle sslBundle = mock(SslBundle.class);
79+
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS.withSslBundle(sslBundle);
80+
assertThat(settings.connectTimeout()).isNull();
81+
assertThat(settings.readTimeout()).isNull();
82+
assertThat(settings.bufferRequestBody()).isNull();
83+
assertThat(settings.sslBundle()).isSameAs(sslBundle);
6784
}
6885

6986
}

0 commit comments

Comments
 (0)