Add SSL bundle support to Redis auto-configuration

Update Redis auto-configuration with Lettuce and Jedis drivers to
allow SSL configuration with an SSL bundle.

Closes gh-34815
pull/35165/head
Scott Frederick 2 years ago
parent 8bbe894665
commit 226c3005d4

@ -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<RedisStandaloneConfiguration> standaloneConfigurationProvider,
ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
ObjectProvider<RedisClusterConfiguration> clusterConfiguration, RedisConnectionDetails connectionDetails) {
ObjectProvider<RedisClusterConfiguration> clusterConfiguration, RedisConnectionDetails connectionDetails,
ObjectProvider<SslBundles> sslBundles) {
super(properties, connectionDetails, standaloneConfigurationProvider, sentinelConfiguration,
clusterConfiguration);
clusterConfiguration, sslBundles);
}
@Bean
@ -81,6 +89,9 @@ class JedisConnectionConfiguration extends RedisConnectionConfiguration {
private JedisClientConfiguration getJedisClientConfiguration(
ObjectProvider<JedisClientConfigurationBuilderCustomizer> 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));

@ -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<RedisStandaloneConfiguration> standaloneConfigurationProvider,
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider,
RedisConnectionDetails connectionDetails) {
RedisConnectionDetails connectionDetails, ObjectProvider<SslBundles> 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();
}

@ -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<RedisStandaloneConfiguration> standaloneConfigurationProvider,
ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider,
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {
ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider,
ObjectProvider<SslBundles> 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;

@ -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.
*/

@ -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"

@ -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");
}

@ -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 <T extends ClientOptions> ContextConsumer<AssertableApplicationContext> assertClientOptions(
Class<T> expectedType, Consumer<T> options) {
return (context) -> {

@ -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 <<features#features.ssl,SSL bundle>> 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]]

Loading…
Cancel
Save