diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java index a7d0b0f7ad..7164319028 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/JedisConnectionConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.boot.autoconfigure.data.redis; +import javax.net.ssl.SSLParameters; + import org.apache.commons.pool2.impl.GenericObjectPool; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPoolConfig; @@ -25,6 +27,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslOptions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; @@ -33,6 +38,7 @@ import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisClientConfigurationBuilder; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration.JedisSslClientConfigurationBuilder; import org.springframework.data.redis.connection.jedis.JedisConnection; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.util.StringUtils; @@ -45,6 +51,7 @@ import org.springframework.util.StringUtils; * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class }) @@ -55,9 +62,10 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { JedisConnectionConfiguration(RedisProperties properties, ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfiguration, - ObjectProvider clusterConfiguration, RedisConnectionDetails connectionDetails) { + ObjectProvider clusterConfiguration, RedisConnectionDetails connectionDetails, + ObjectProvider sslBundles) { super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfiguration, - clusterConfiguration); + clusterConfiguration, sslBundles); } @Bean @@ -81,6 +89,9 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { private JedisClientConfiguration getJedisClientConfiguration( ObjectProvider builderCustomizers) { JedisClientConfigurationBuilder builder = applyProperties(JedisClientConfiguration.builder()); + if (isSslEnabled()) { + applySsl(builder); + } RedisProperties.Pool pool = getProperties().getJedis().getPool(); if (isPoolEnabled(pool)) { applyPooling(pool, builder); @@ -94,15 +105,26 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration { private JedisClientConfigurationBuilder applyProperties(JedisClientConfigurationBuilder builder) { PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); - boolean ssl = (!(getConnectionDetails() instanceof PropertiesRedisConnectionDetails)) ? false - : getProperties().isSsl(); - map.from(ssl).whenTrue().toCall(builder::useSsl); map.from(getProperties().getTimeout()).to(builder::readTimeout); map.from(getProperties().getConnectTimeout()).to(builder::connectTimeout); map.from(getProperties().getClientName()).whenHasText().to(builder::clientName); return builder; } + private void applySsl(JedisClientConfigurationBuilder builder) { + JedisSslClientConfigurationBuilder sslBuilder = builder.useSsl(); + if (getProperties().getSsl().getBundle() != null) { + SslBundle sslBundle = getSslBundles().getBundle(getProperties().getSsl().getBundle()); + sslBuilder.sslSocketFactory(sslBundle.createSslContext().getSocketFactory()); + SslOptions sslOptions = sslBundle.getOptions(); + SSLParameters sslParameters = new SSLParameters(); + PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); + map.from(SslOptions.toArray(sslOptions.getCiphers())).to(sslParameters::setCipherSuites); + map.from(SslOptions.toArray(sslOptions.getEnabledProtocols())).to(sslParameters::setProtocols); + sslBuilder.sslParameters(sslParameters); + } + } + private void applyPooling(RedisProperties.Pool pool, JedisClientConfiguration.JedisClientConfigurationBuilder builder) { builder.usePooling().poolConfig(jedisPoolConfig(pool)); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java index 7d21a77f63..9f1b6d74d5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.java @@ -35,6 +35,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Lettuce.Cluster.Refresh; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslOptions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisClusterConfiguration; @@ -54,6 +57,7 @@ import org.springframework.util.StringUtils; * @author Andy Wilkinson * @author Moritz Halbritter * @author Phillip Webb + * @author Scott Frederick */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass(RedisClient.class) @@ -64,9 +68,9 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, ObjectProvider clusterConfigurationProvider, - RedisConnectionDetails connectionDetails) { + RedisConnectionDetails connectionDetails, ObjectProvider sslBundles) { super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfigurationProvider, - clusterConfigurationProvider); + clusterConfigurationProvider, sslBundles); } @Bean(destroyMethod = "shutdown") @@ -118,9 +122,8 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { return LettuceClientConfiguration.builder(); } - private LettuceClientConfigurationBuilder applyProperties( - LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) { - if (getConnectionDetails() instanceof PropertiesRedisConnectionDetails && getProperties().isSsl()) { + private void applyProperties(LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) { + if (isSslEnabled()) { builder.useSsl(); } if (getProperties().getTimeout() != null) { @@ -135,7 +138,6 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { if (StringUtils.hasText(getProperties().getClientName())) { builder.clientName(getProperties().getClientName()); } - return builder; } private ClientOptions createClientOptions() { @@ -144,6 +146,21 @@ class LettuceConnectionConfiguration extends RedisConnectionConfiguration { if (connectTimeout != null) { builder.socketOptions(SocketOptions.builder().connectTimeout(connectTimeout).build()); } + if (isSslEnabled() && getProperties().getSsl().getBundle() != null) { + SslBundle sslBundle = getSslBundles().getBundle(getProperties().getSsl().getBundle()); + io.lettuce.core.SslOptions.Builder sslOptionsBuilder = io.lettuce.core.SslOptions.builder(); + sslOptionsBuilder.keyManager(sslBundle.getManagers().getKeyManagerFactory()); + sslOptionsBuilder.trustManager(sslBundle.getManagers().getTrustManagerFactory()); + String[] ciphers = SslOptions.toArray(sslBundle.getOptions().getCiphers()); + if (ciphers != null) { + sslOptionsBuilder.cipherSuites(ciphers); + } + String[] protocols = SslOptions.toArray(sslBundle.getOptions().getEnabledProtocols()); + if (protocols != null) { + sslOptionsBuilder.protocols(protocols); + } + builder.sslOptions(sslOptionsBuilder.build()); + } return builder.timeoutOptions(TimeoutOptions.enabled()).build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java index 733e063875..850de433ef 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisConnectionConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails. import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails.Node; import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails.Sentinel; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool; +import org.springframework.boot.ssl.SslBundles; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisPassword; @@ -60,15 +61,19 @@ abstract class RedisConnectionConfiguration { private final RedisConnectionDetails connectionDetails; + private final SslBundles sslBundles; + protected RedisConnectionConfiguration(RedisProperties properties, RedisConnectionDetails connectionDetails, ObjectProvider standaloneConfigurationProvider, ObjectProvider sentinelConfigurationProvider, - ObjectProvider clusterConfigurationProvider) { + ObjectProvider clusterConfigurationProvider, + ObjectProvider sslBundles) { this.properties = properties; this.standaloneConfiguration = standaloneConfigurationProvider.getIfAvailable(); this.sentinelConfiguration = sentinelConfigurationProvider.getIfAvailable(); this.clusterConfiguration = clusterConfigurationProvider.getIfAvailable(); this.connectionDetails = connectionDetails; + this.sslBundles = sslBundles.getIfAvailable(); } protected final RedisStandaloneConfiguration getStandaloneConfig() { @@ -141,6 +146,15 @@ abstract class RedisConnectionConfiguration { return this.properties; } + protected SslBundles getSslBundles() { + return this.sslBundles; + } + + protected boolean isSslEnabled() { + return getConnectionDetails() instanceof PropertiesRedisConnectionDetails + && getProperties().getSsl().isEnabled(); + } + protected boolean isPoolEnabled(Pool pool) { Boolean enabled = pool.getEnabled(); return (enabled != null) ? enabled : COMMONS_POOL2_AVAILABLE; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java index 7ceb073b4a..c66e8fde7b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/data/redis/RedisProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -30,6 +30,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; * @author Marco Aust * @author Mark Paluch * @author Stephane Nicoll + * @author Scott Frederick * @since 1.0.0 */ @ConfigurationProperties(prefix = "spring.data.redis") @@ -66,11 +67,6 @@ public class RedisProperties { */ private int port = 6379; - /** - * Whether to enable SSL support. - */ - private boolean ssl; - /** * Read timeout. */ @@ -95,6 +91,8 @@ public class RedisProperties { private Cluster cluster; + private final Ssl ssl = new Ssl(); + private final Jedis jedis = new Jedis(); private final Lettuce lettuce = new Lettuce(); @@ -147,14 +145,10 @@ public class RedisProperties { this.port = port; } - public boolean isSsl() { + public Ssl getSsl() { return this.ssl; } - public void setSsl(boolean ssl) { - this.ssl = ssl; - } - public void setTimeout(Duration timeout) { this.timeout = timeout; } @@ -416,6 +410,37 @@ public class RedisProperties { } + public static class Ssl { + + /** + * Whether to enable SSL support. Enabled automatically if "bundle" is provided + * unless specified otherwise. + */ + private Boolean enabled; + + /** + * SSL bundle name. + */ + private String bundle; + + public boolean isEnabled() { + return (this.enabled != null) ? this.enabled : this.bundle != null; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getBundle() { + return this.bundle; + } + + public void setBundle(String bundle) { + this.bundle = bundle; + } + + } + /** * Jedis client properties. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 404163ff18..d4c480df64 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1104,6 +1104,14 @@ "description": "Whether to enable Redis repositories.", "defaultValue": true }, + { + "name": "spring.data.redis.ssl", + "type": "java.lang.Boolean", + "deprecation": { + "replacement": "spring.data.redis.ssl.enabled", + "level": "error" + } + }, { "name": "spring.data.rest.detection-strategy", "defaultValue": "default" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java index f6636a9fb1..164e222fd8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationJedisTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.context.annotation.Bean; @@ -42,12 +43,13 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Moritz Halbritter * @author Andy Wilkinson * @author Phillip Webb + * @author Scott Frederick */ @ClassPathExclusions("lettuce-core-*.jar") class RedisAutoConfigurationJedisTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, SslAutoConfiguration.class)); @Test void connectionFactoryDefaultsToJedis() { @@ -108,7 +110,7 @@ class RedisAutoConfigurationJedisTests { void testOverrideUrlRedisConfiguration() { this.contextRunner .withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.password:xyz", - "spring.data.redis.port:1000", "spring.data.redis.ssl:false", + "spring.data.redis.port:1000", "spring.data.redis.ssl.enabled:false", "spring.data.redis.url:rediss://user:password@example:33") .run((context) -> { JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); @@ -237,6 +239,37 @@ class RedisAutoConfigurationJedisTests { .isTrue()); } + @Test + void testRedisConfigurationWithSslEnabled() { + this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> { + JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); + assertThat(cf.isUseSsl()).isTrue(); + }); + } + + @Test + void testRedisConfigurationWithSslBundle() { + this.contextRunner + .withPropertyValues("spring.data.redis.ssl.bundle:test-bundle", + "spring.ssl.bundle.jks.test-bundle.keystore.location:classpath:test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.password:secret", + "spring.ssl.bundle.jks.test-bundle.key.password:password") + .run((context) -> { + JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); + assertThat(cf.isUseSsl()).isTrue(); + }); + } + + @Test + void testRedisConfigurationWithSslDisabledAndBundle() { + this.contextRunner + .withPropertyValues("spring.data.redis.ssl.enabled:false", "spring.data.redis.ssl.bundle:test-bundle") + .run((context) -> { + JedisConnectionFactory cf = context.getBean(JedisConnectionFactory.class); + assertThat(cf.isUseSsl()).isFalse(); + }); + } + private String getUserName(JedisConnectionFactory factory) { return ReflectionTestUtils.invokeMethod(factory, "getRedisUsername"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java index 2b4a14d2e1..47e3e86d95 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/data/redis/RedisAutoConfigurationTests.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.data.redis.RedisProperties.Pool; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; @@ -79,7 +80,7 @@ import static org.mockito.Mockito.mock; class RedisAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class, SslAutoConfiguration.class)); @Test void testDefaultRedisConfiguration() { @@ -143,7 +144,7 @@ class RedisAutoConfigurationTests { void testOverrideUrlRedisConfiguration() { this.contextRunner .withPropertyValues("spring.data.redis.host:foo", "spring.data.redis.password:xyz", - "spring.data.redis.port:1000", "spring.data.redis.ssl:false", + "spring.data.redis.port:1000", "spring.data.redis.ssl.enabled:false", "spring.data.redis.url:rediss://user:password@example:33") .run((context) -> { LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); @@ -550,6 +551,37 @@ class RedisAutoConfigurationTests { }); } + @Test + void testRedisConfigurationWithSslEnabled() { + this.contextRunner.withPropertyValues("spring.data.redis.ssl.enabled:true").run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.isUseSsl()).isTrue(); + }); + } + + @Test + void testRedisConfigurationWithSslBundle() { + this.contextRunner + .withPropertyValues("spring.data.redis.ssl.bundle:test-bundle", + "spring.ssl.bundle.jks.test-bundle.keystore.location:classpath:test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.password:secret", + "spring.ssl.bundle.jks.test-bundle.key.password:password") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.isUseSsl()).isTrue(); + }); + } + + @Test + void testRedisConfigurationWithSslDisabledBundle() { + this.contextRunner + .withPropertyValues("spring.data.redis.ssl.enabled:false", "spring.data.redis.ssl.bundle:test-bundle") + .run((context) -> { + LettuceConnectionFactory cf = context.getBean(LettuceConnectionFactory.class); + assertThat(cf.isUseSsl()).isFalse(); + }); + } + private ContextConsumer assertClientOptions( Class expectedType, Consumer options) { return (context) -> { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc index f2a4355073..be74814f26 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc @@ -62,6 +62,27 @@ If you add your own `@Bean` of any of the auto-configured types, it replaces the By default, a pooled connection factory is auto-configured if `commons-pool2` is on the classpath. +The auto-configured `RedisConnectionFactory` can be configured to use SSL for communication with the server by setting the properties as shown in this example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + redis: + ssl: + enabled: true +---- + +Custom SSL trust material can be configured in an <> and applied to the `RedisConnectionFactory` as shown in this example: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + data: + redis: + ssl: + bundle: "example" +---- [[data.nosql.mongodb]]