diff --git a/pom.xml b/pom.xml
index 8e149f902c..58f8ab1a9e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-redis
- 2.3.0.BUILD-SNAPSHOT
+ 2.3.0.DATAREDIS-1060-SNAPSHOT
Spring Data Redis
diff --git a/src/main/asciidoc/reference/redis.adoc b/src/main/asciidoc/reference/redis.adoc
index 4ffbc75c3e..96be9c7ee1 100644
--- a/src/main/asciidoc/reference/redis.adoc
+++ b/src/main/asciidoc/reference/redis.adoc
@@ -174,13 +174,14 @@ public RedisConnectionFactory lettuceConnectionFactory() {
.Configuration Properties
* `spring.redis.sentinel.master`: name of the master node.
* `spring.redis.sentinel.nodes`: Comma delimited list of host:port pairs.
+* `spring.redis.sentinel.password`: The password to apply when authenticating with Redis Sentinel
====
Sometimes, direct interaction with one of the Sentinels is required. Using `RedisConnectionFactory.getSentinelConnection()` or `RedisConnection.getSentinelCommands()` gives you access to the first active Sentinel configured.
[NOTE]
====
-Configuration options like password, timeouts, SSL... also get applied to Redis Sentinel when using https://lettuce.io/[Lettuce].
+Sentinel authentication is only available using https://lettuce.io/[Lettuce].
====
[[redis:template]]
diff --git a/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java
index 0af18b1b4d..90a8a27121 100644
--- a/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java
+++ b/src/main/java/org/springframework/data/redis/connection/RedisConfiguration.java
@@ -337,6 +337,56 @@ default void setMaster(final String name) {
* @return {@link Set} of sentinels. Never {@literal null}.
*/
Set getSentinels();
+
+ /**
+ * Get the {@link RedisPassword} used when authenticating with a Redis Server..
+ *
+ * @return never {@literal null}.
+ * @since 2.2.2
+ */
+ default RedisPassword getDataNodePassword() {
+ return getPassword();
+ }
+
+ /**
+ * Create and set a {@link RedisPassword} to be used when authenticating with Redis Sentinel from the given
+ * {@link String}.
+ *
+ * @param password can be {@literal null}.
+ * @since 2.2.2
+ */
+ default void setSentinelPassword(@Nullable String password) {
+ setSentinelPassword(RedisPassword.of(password));
+ }
+
+ /**
+ * Create and set a {@link RedisPassword} to be used when authenticating with Redis Sentinel from the given
+ * {@link Character} sequence.
+ *
+ * @param password can be {@literal null}.
+ * @since 2.2.2
+ */
+ default void setSentinelPassword(@Nullable char[] password) {
+ setSentinelPassword(RedisPassword.of(password));
+ }
+
+ /**
+ * Set a {@link RedisPassword} to be used when authenticating with Redis Sentinel.
+ *
+ * @param password must not be {@literal null} use {@link RedisPassword#none()} instead.
+ * @since 2.2.2
+ */
+ void setSentinelPassword(RedisPassword password);
+
+ /**
+ * Returns the {@link RedisPassword} to use when connecting to a Redis Sentinel.
+ * Can be set via {@link #setSentinelPassword(RedisPassword)} or {@link RedisPassword#none()} if no password has
+ * been set.
+ *
+ * @return the {@link RedisPassword} for authenticating with Redis Sentinel.
+ * @since 2.2.2
+ */
+ RedisPassword getSentinelPassword();
}
/**
diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java
index a5d34227ab..a28b4d2129 100644
--- a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java
+++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java
@@ -15,8 +15,6 @@
*/
package org.springframework.data.redis.connection;
-import static org.springframework.util.Assert.*;
-import static org.springframework.util.Assert.hasText;
import static org.springframework.util.StringUtils.*;
import java.util.Collections;
@@ -46,11 +44,14 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC
private static final String REDIS_SENTINEL_MASTER_CONFIG_PROPERTY = "spring.redis.sentinel.master";
private static final String REDIS_SENTINEL_NODES_CONFIG_PROPERTY = "spring.redis.sentinel.nodes";
+ private static final String REDIS_SENTINEL_PASSWORD_CONFIG_PROPERTY = "spring.redis.sentinel.password";
private @Nullable NamedNode master;
private Set sentinels;
private int database;
- private RedisPassword password = RedisPassword.none();
+
+ private RedisPassword dataNodePassword = RedisPassword.none();
+ private RedisPassword sentinelPassword = RedisPassword.none();
/**
* Creates new {@link RedisSentinelConfiguration}.
@@ -89,7 +90,7 @@ public RedisSentinelConfiguration(String master, Set sentinelHostAndPort
*/
public RedisSentinelConfiguration(PropertySource> propertySource) {
- notNull(propertySource, "PropertySource must not be null!");
+ Assert.notNull(propertySource, "PropertySource must not be null!");
this.sentinels = new LinkedHashSet<>();
@@ -101,6 +102,10 @@ public RedisSentinelConfiguration(PropertySource> propertySource) {
appendSentinels(
commaDelimitedListToSet(propertySource.getProperty(REDIS_SENTINEL_NODES_CONFIG_PROPERTY).toString()));
}
+
+ if (propertySource.containsProperty(REDIS_SENTINEL_PASSWORD_CONFIG_PROPERTY)) {
+ this.setSentinelPassword(propertySource.getProperty(REDIS_SENTINEL_PASSWORD_CONFIG_PROPERTY).toString());
+ }
}
/**
@@ -110,7 +115,7 @@ public RedisSentinelConfiguration(PropertySource> propertySource) {
*/
public void setSentinels(Iterable sentinels) {
- notNull(sentinels, "Cannot set sentinels to 'null'.");
+ Assert.notNull(sentinels, "Cannot set sentinels to 'null'.");
this.sentinels.clear();
@@ -134,7 +139,7 @@ public Set getSentinels() {
*/
public void addSentinel(RedisNode sentinel) {
- notNull(sentinel, "Sentinel must not be 'null'.");
+ Assert.notNull(sentinel, "Sentinel must not be 'null'.");
this.sentinels.add(sentinel);
}
@@ -144,7 +149,7 @@ public void addSentinel(RedisNode sentinel) {
*/
public void setMaster(NamedNode master) {
- notNull(master, "Sentinel master node must not be 'null'.");
+ Assert.notNull(master, "Sentinel master node must not be 'null'.");
this.master = master;
}
@@ -230,7 +235,7 @@ public void setDatabase(int index) {
*/
@Override
public RedisPassword getPassword() {
- return password;
+ return dataNodePassword;
}
/*
@@ -242,15 +247,34 @@ public void setPassword(RedisPassword password) {
Assert.notNull(password, "RedisPassword must not be null!");
- this.password = password;
+ this.dataNodePassword = password;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.redis.connection.RedisConfiguration.SentinelConfiguration#setSentinelPassword(org.springframework.data.redis.connection.RedisPassword)
+ */
+ public void setSentinelPassword(RedisPassword sentinelPassword) {
+
+ Assert.notNull(sentinelPassword, "SentinelPassword must not be null!");
+ this.sentinelPassword = sentinelPassword;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.redis.connection.RedisConfiguration.SentinelConfiguration#setSentinelPassword()
+ */
+ @Override
+ public RedisPassword getSentinelPassword() {
+ return sentinelPassword;
}
private RedisNode readHostAndPortFromString(String hostAndPort) {
String[] args = split(hostAndPort, ":");
- notNull(args, "HostAndPort need to be seperated by ':'.");
- isTrue(args.length == 2, "Host and Port String needs to specified as host:port");
+ Assert.notNull(args, "HostAndPort need to be seperated by ':'.");
+ Assert.isTrue(args.length == 2, "Host and Port String needs to specified as host:port");
return new RedisNode(args[0], Integer.valueOf(args[1]).intValue());
}
@@ -261,8 +285,8 @@ private RedisNode readHostAndPortFromString(String hostAndPort) {
*/
private static Map asMap(String master, Set sentinelHostAndPorts) {
- hasText(master, "Master address must not be null or empty!");
- notNull(sentinelHostAndPorts, "SentinelHostAndPorts must not be null!");
+ Assert.hasText(master, "Master address must not be null or empty!");
+ Assert.notNull(sentinelHostAndPorts, "SentinelHostAndPorts must not be null!");
Map map = new HashMap<>();
map.put(REDIS_SENTINEL_MASTER_CONFIG_PROPERTY, master);
diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
index eaa1875e37..07eb48d2f2 100644
--- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
+++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java
@@ -1080,7 +1080,6 @@ private RedisURI getSentinelRedisURI() {
applyToAll(redisUri, it -> {
- getRedisPassword().toOptional().ifPresent(it::setPassword);
clientConfiguration.getClientName().ifPresent(it::setClientName);
it.setSsl(clientConfiguration.isUseSsl());
diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java
index 1a6fb1af6a..7ff1a37b2b 100644
--- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java
+++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java
@@ -50,6 +50,7 @@
import org.springframework.data.redis.connection.RedisListCommands.Position;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisNode.NodeType;
+import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisServer;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
@@ -198,7 +199,8 @@ abstract public class LettuceConverters extends Converters {
};
SCORED_VALUE_TO_TUPLE = source -> source != null
- ? new DefaultTuple(source.getValue(), Double.valueOf(source.getScore())) : null;
+ ? new DefaultTuple(source.getValue(), Double.valueOf(source.getScore()))
+ : null;
BYTES_LIST_TO_TUPLE_LIST_CONVERTER = source -> {
@@ -297,7 +299,8 @@ private Set parseFlags(Set source) {
};
GEO_COORDINATE_TO_POINT_CONVERTER = geoCoordinate -> geoCoordinate != null
- ? new Point(geoCoordinate.getX().doubleValue(), geoCoordinate.getY().doubleValue()) : null;
+ ? new Point(geoCoordinate.getX().doubleValue(), geoCoordinate.getY().doubleValue())
+ : null;
GEO_COORDINATE_LIST_TO_POINT_LIST_CONVERTER = new ListConverter<>(GEO_COORDINATE_TO_POINT_CONVERTER);
KEY_VALUE_UNWRAPPER = source -> source.getValueOrElse(null);
@@ -637,17 +640,25 @@ public static RedisURI sentinelConfigurationToRedisURI(RedisSentinelConfiguratio
Assert.notNull(sentinelConfiguration, "RedisSentinelConfiguration is required");
Set sentinels = sentinelConfiguration.getSentinels();
- RedisURI.Builder builder = null;
+ RedisPassword sentinelPassword = sentinelConfiguration.getSentinelPassword();
+ RedisURI.Builder builder = RedisURI.builder();
for (RedisNode sentinel : sentinels) {
- if (builder == null) {
- builder = RedisURI.Builder.sentinel(sentinel.getHost(), sentinel.getPort(),
- sentinelConfiguration.getMaster().getName());
- } else {
- builder.withSentinel(sentinel.getHost(), sentinel.getPort());
+ RedisURI.Builder sentinelBuilder = RedisURI.Builder.redis(sentinel.getHost(), sentinel.getPort());
+
+ if (sentinelPassword.isPresent()) {
+ sentinelBuilder.withPassword(sentinelPassword.get());
}
+ builder.withSentinel(sentinelBuilder.build());
}
+ RedisPassword password = sentinelConfiguration.getPassword();
+ if (password.isPresent()) {
+ builder.withPassword(password.get());
+ }
+
+ builder.withSentinelMasterId(sentinelConfiguration.getMaster().getName());
+
return builder.build();
}
diff --git a/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java
index 95aabc3eb9..35100ea25d 100644
--- a/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java
+++ b/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java
@@ -22,7 +22,6 @@
import java.util.HashSet;
import org.junit.Test;
-
import org.springframework.core.env.PropertySource;
import org.springframework.mock.env.MockPropertySource;
import org.springframework.util.StringUtils;
@@ -51,8 +50,7 @@ public void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndSingleH
public void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndMultipleHostAndPortStrings() {
RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster",
- new HashSet<>(Arrays.asList(
- HOST_AND_PORT_1, HOST_AND_PORT_2, HOST_AND_PORT_3)));
+ new HashSet<>(Arrays.asList(HOST_AND_PORT_1, HOST_AND_PORT_2, HOST_AND_PORT_3)));
assertThat(config.getSentinels().size()).isEqualTo(3);
assertThat(config.getSentinels()).contains(new RedisNode("127.0.0.1", 123), new RedisNode("localhost", 456),
@@ -121,4 +119,29 @@ public void shouldBeCreatedCorrecltyGivenValidPropertySourceWithMasterAndMultipl
assertThat(config.getSentinels()).contains(new RedisNode("127.0.0.1", 123), new RedisNode("localhost", 456),
new RedisNode("localhost", 789));
}
+
+ @Test // DATAREDIS-1060
+ public void dataNodePasswordDoesNotAffectSentinelPassword() {
+
+ RedisPassword password = RedisPassword.of("88888888-8x8-getting-creative-now");
+ RedisSentinelConfiguration configuration = new RedisSentinelConfiguration("myMaster",
+ Collections.singleton(HOST_AND_PORT_1));
+ configuration.setPassword(password);
+
+ assertThat(configuration.getSentinelPassword()).isEqualTo(RedisPassword.none());
+ }
+
+ @Test // DATAREDIS-1060
+ public void readSentinelPasswordFromConfigProperty() {
+
+ MockPropertySource propertySource = new MockPropertySource();
+ propertySource.setProperty("spring.redis.sentinel.master", "myMaster");
+ propertySource.setProperty("spring.redis.sentinel.nodes", HOST_AND_PORT_1);
+ propertySource.setProperty("spring.redis.sentinel.password", "computer-says-no");
+
+ RedisSentinelConfiguration config = new RedisSentinelConfiguration(propertySource);
+
+ assertThat(config.getSentinelPassword()).isEqualTo(RedisPassword.of("computer-says-no"));
+ assertThat(config.getSentinels()).hasSize(1).contains(new RedisNode("127.0.0.1", 123));
+ }
}
diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java
index 15fe5a61e9..04e7ef0cae 100644
--- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java
+++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryUnitTests.java
@@ -46,7 +46,6 @@
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
-
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.data.redis.ConnectionFactoryTracker;
@@ -179,8 +178,8 @@ public void passwordShouldBeSetCorrectlyOnClusterClient() {
}
}
- @Test // DATAREDIS-524, DATAREDIS-1045
- public void passwordShouldBeSetCorrectlyOnSentinelClient() {
+ @Test // DATAREDIS-524, DATAREDIS-1045, DATAREDIS-1060
+ public void passwordShouldNotBeSetOnSentinelClient() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(
new RedisSentinelConfiguration("mymaster", Collections.singleton("host:1234")));
@@ -197,8 +196,54 @@ public void passwordShouldBeSetCorrectlyOnSentinelClient() {
assertThat(redisUri.getPassword()).isEqualTo(connectionFactory.getPassword().toCharArray());
for (RedisURI sentinel : redisUri.getSentinels()) {
- assertThat(sentinel.getPassword())
- .isEqualTo(connectionFactory.getPassword().toCharArray());
+ assertThat(sentinel.getPassword()).isNull();
+ }
+ }
+
+ @Test // DATAREDIS-1060
+ public void sentinelPasswordShouldBeSetOnSentinelClient() {
+
+ RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster", Collections.singleton("host:1234"));
+ config.setSentinelPassword("sentinel-pwd");
+
+ LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(config);
+ connectionFactory.setClientResources(getSharedClientResources());
+ connectionFactory.setPassword("o_O");
+ connectionFactory.afterPropertiesSet();
+ ConnectionFactoryTracker.add(connectionFactory);
+
+ AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client");
+ assertThat(client).isInstanceOf(RedisClient.class);
+
+ RedisURI redisUri = (RedisURI) getField(client, "redisURI");
+
+ assertThat(redisUri.getPassword()).isEqualTo(connectionFactory.getPassword().toCharArray());
+
+ for (RedisURI sentinel : redisUri.getSentinels()) {
+ assertThat(sentinel.getPassword()).isEqualTo("sentinel-pwd".toCharArray());
+ }
+ }
+
+ @Test // DATAREDIS-1060
+ public void sentinelPasswordShouldNotLeakIntoDataNodeClient() {
+
+ RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster", Collections.singleton("host:1234"));
+ config.setSentinelPassword("sentinel-pwd");
+
+ LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(config);
+ connectionFactory.setClientResources(getSharedClientResources());
+ connectionFactory.afterPropertiesSet();
+ ConnectionFactoryTracker.add(connectionFactory);
+
+ AbstractRedisClient client = (AbstractRedisClient) getField(connectionFactory, "client");
+ assertThat(client).isInstanceOf(RedisClient.class);
+
+ RedisURI redisUri = (RedisURI) getField(client, "redisURI");
+
+ assertThat(redisUri.getPassword()).isNull();
+
+ for (RedisURI sentinel : redisUri.getSentinels()) {
+ assertThat(sentinel.getPassword()).isEqualTo("sentinel-pwd".toCharArray());
}
}