Lettuce can't connect to Redis Cluster using SSL but can connect to same Redis server using SSL by treating it as a Standalone node

huangapple go评论90阅读模式
英文:

Lettuce can't connect to Redis Cluster using SSL but can connect to same Redis server using SSL by treating it as a Standalone node

问题

我有一个启用了 Azure Cache for Redis - Premium 和集群的 Redis。我一直在尝试使用 spring-boot-starter-data-redis(Spring Boot 版本:2.3.4.RELEASE,Java 版本:11)和 Lettuce 客户端连接到该 Redis,但是当我将我的 Redis 视为 Redis 集群时,Lettuce 抛出以下 SSL 异常,但在将其作为独立的 Redis 服务器使用时连接正常。

我的 pom.xml 依赖项如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

Java 代码:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;

@Configuration
class LettuceConfig {

    @Bean
    StringRedisTemplate getStringRedisTemplate(final RedisProperties redisProperties) {
        return new StringRedisTemplate(getRedisConnectionFactory(redisProperties));
    }

    @Bean
    RedisConnectionFactory getRedisConnectionFactory(final RedisProperties redisProperties) {
        
        final RedisNode redisNode = RedisNode.newRedisNode()
                .listeningAt(redisProperties.getHost(), redisProperties.getPort())
                .build();

        // Connecting as a Redis Cluster
        final RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
        redisClusterConfiguration.addClusterNode(redisNode);
        redisClusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));

        // Connecting as a Standalone Redis server
        final RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisProperties.getHost());
        redisStandaloneConfiguration.setPort(redisProperties.getPort());
        redisStandaloneConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));

        final LettuceClientConfiguration.LettuceClientConfigurationBuilder lettuceClientConfigurationBuilder =
                LettuceClientConfiguration.builder()
                .clientName(redisProperties.getClientName())
                .commandTimeout(redisProperties.getTimeout());

        if (redisProperties.isSsl()) {
            lettuceClientConfigurationBuilder.useSsl();
        }

        final LettuceClientConfiguration lettuceClientConfiguration = lettuceClientConfigurationBuilder.build();

        return new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
    }
}

@SpringBootApplication
public class LettuceClusterApplication implements CommandLineRunner {

    private final StringRedisTemplate stringRedisTemplate;

    @Autowired
    public LettuceClusterApplication(final StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public static void main(String[] args) {
        SpringApplication.run(LettuceClusterApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        System.out.println(stringRedisTemplate.hasKey("abc"));
    }
}

当在 new LettuceConnectionFactory(..., ...) 中使用 redisStandaloneConfiguration 时,代码运行正常。但是如果我使用 redisClusterConfiguration,代码将会因以下异常而失败:

java.lang.IllegalStateException: Failed to execute CommandLineRunner
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798) ~[spring-boot-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    ...
Caused by: org.springframework.data.redis.RedisConnectionFailureException: Redis connection failed; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to [RedisURI [host='<redacted>.redis.cache.windows.net', port=6380]]
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:66) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
    ...
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to [RedisURI [host='<redacted>.redis.cache.windows.net', port=6380]]
    at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:78) ~[lettuce-core-5.3.4.RELEASE.jar:5.3.4.RELEASE]
    ...
Caused by: javax.net.ssl.SSLHandshakeException: No subject alternative names matching IP address <redacted> found
    ...
Caused by: java.security.cert.CertificateException: No subject alternative names matching IP address <redacted> found
    at java.base/sun.security.util.HostnameChecker.matchIP(HostnameChecker.java:165) ~[na:na]
    ...

我的 application.properties 文件:

spring.redis.host = <redacted>.redis.cache.windows.net
spring.redis.port = 6380
spring.redis.password = <redacted>
spring.redis.ssl = true
spring.redis.clientName = ${HOSTNAME}
spring.redis.timeout = 100000

更新:在 GitHub 上找到了一个类似的问题:https://github.com/lettuce-io/lettuce-core/issues/246,但是它表示应该与 Lettuce 版本大于 4.2 一起使用,而我的 Lettuce 版本(在 spring-boot-starter-data-redis 下捆绑)是 5.3.4.RELEASE。还值得查看的是文档,文档中也是相同的内容:https://lettuce.io/core/release/reference/#ssl

Lettuce 自版本 3.1 起支持 Redis 独立连接的 SSL 连接,自版本 4.2 起支持 Redis 集群

还在 GitHub 上提出了问题:https://github.com/lettuce-io/lettuce-core/issues/1454

英文:

I've an Azure Cache for Redis - Premium and Cluster enabled. I've been trying to connect to that Redis using spring-boot-starter-data-redis (spring boot version: 2.3.4.RELEASE, Java version: 11) and using lettuce client but Lettuce is throwing the following SSL exception when I am treating my Redis as a Redis Cluster but connects just fine when using it as a Standalone Redis server.

My pom.xml dependencies are:

&lt;dependencies&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-data-redis&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
&lt;scope&gt;test&lt;/scope&gt;
&lt;exclusions&gt;
&lt;exclusion&gt;
&lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;
&lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;
&lt;/exclusion&gt;
&lt;/exclusions&gt;
&lt;/dependency&gt;
&lt;/dependencies&gt;

Java code:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
class LettuceConfig {
@Bean
StringRedisTemplate getStringRedisTemplate(final RedisProperties redisProperties) {
return new StringRedisTemplate(getRedisConnectionFactory(redisProperties));
}
@Bean
RedisConnectionFactory getRedisConnectionFactory(final RedisProperties redisProperties) {
final RedisNode redisNode = RedisNode.newRedisNode()
.listeningAt(redisProperties.getHost(), redisProperties.getPort())
.build();
// Connecting as a Redis Cluster
final RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
redisClusterConfiguration.addClusterNode(redisNode);
redisClusterConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
// Connecting as a Standalone Redis server
final RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(redisProperties.getHost());
redisStandaloneConfiguration.setPort(redisProperties.getPort());
redisStandaloneConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
final LettuceClientConfiguration.LettuceClientConfigurationBuilder lettuceClientConfigurationBuilder =
LettuceClientConfiguration.builder()
.clientName(redisProperties.getClientName())
.commandTimeout(redisProperties.getTimeout());
if (redisProperties.isSsl()) {
lettuceClientConfigurationBuilder.useSsl();
}
final LettuceClientConfiguration lettuceClientConfiguration = lettuceClientConfigurationBuilder.build();
return new LettuceConnectionFactory(redisClusterConfiguration, lettuceClientConfiguration);
}
}
@SpringBootApplication
public class LettuceClusterApplication implements CommandLineRunner {
private final StringRedisTemplate stringRedisTemplate;
@Autowired
public LettuceClusterApplication(final StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public static void main(String[] args) {
SpringApplication.run(LettuceClusterApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
System.out.println(stringRedisTemplate.hasKey(&quot;abc&quot;));
}
}

When using redisStandaloneConfiguration in new LettuceConnectionFactory(..., ...), the code works just fine, but if I use redisClusterConfiguration, the code fails with the following exception:

java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798) ~[spring-boot-2.3.4.RELEASE.jar:2.3.4.RELEASE]
...
Caused by: org.springframework.data.redis.RedisConnectionFailureException: Redis connection failed; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to [RedisURI [host=&#39;&lt;redacted&gt;.redis.cache.windows.net&#39;, port=6380]]
at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:66) ~[spring-data-redis-2.3.4.RELEASE.jar:2.3.4.RELEASE]
...
Caused by: io.lettuce.core.RedisConnectionException: Unable to connect to [RedisURI [host=&#39;&lt;redacted&gt;.redis.cache.windows.net&#39;, port=6380]]
at io.lettuce.core.RedisConnectionException.create(RedisConnectionException.java:78) ~[lettuce-core-5.3.4.RELEASE.jar:5.3.4.RELEASE]
...
Caused by: javax.net.ssl.SSLHandshakeException: No subject alternative names matching IP address &lt;redacted&gt; found
...
Caused by: java.security.cert.CertificateException: No subject alternative names matching IP address &lt;redacted&gt; found
at java.base/sun.security.util.HostnameChecker.matchIP(HostnameChecker.java:165) ~[na:na]
...

My application.properties file:

spring.redis.host = &lt;redacted&gt;.redis.cache.windows.net
spring.redis.port = 6380
spring.redis.password = &lt;redacted&gt;
spring.redis.ssl = true
spring.redis.clientName = ${HOSTNAME}
spring.redis.timeout = 100000

Update: Found a similar issue in Github: https://github.com/lettuce-io/lettuce-core/issues/246 but it says that it should work with lettuce versions > 4.2 and my lettuce-core version (bundled under spring-boot-starter-data-redis) is 5.3.4.RELEASE.
Also worth checking out is the documentation which states the same: https://lettuce.io/core/release/reference/#ssl

> Lettuce supports SSL connections since version 3.1 on Redis Standalone connections and since version 4.2 on Redis Cluster

Raised GitHub issue as well: https://github.com/lettuce-io/lettuce-core/issues/1454

答案1

得分: 5

如果您的所有节点的IP地址与主机名相同(在Azure Cache for Redis中是这种情况,我想),那么这是您配置客户端以将未解析的IP地址映射回证书中列出的主机名的一种方法。

@Bean
ClientResources clientResources(RedisProperties redisProperties) throws UnknownHostException {
    var clientResourcesBuilder = DefaultClientResources.builder();
    var configuredHost = redisProperties.getHost();
    var inetAddresses = Arrays.asList(InetAddress.getAllByName(configuredHost));
    MappingSocketAddressResolver resolver = MappingSocketAddressResolver.create(
        DnsResolvers.UNRESOLVED,
        hostAndPort -> inetAddresses.stream()
            .anyMatch(i -> i.getHostAddress().equals(hostAndPort.getHostText())) ?
            HostAndPort.of(configuredHost, hostAndPort.getPort()) :
            hostAndPort
    );
    clientResourcesBuilder.socketAddressResolver(resolver);
    return clientResourcesBuilder.build();
}

据我所了解,这是GitHub上问题列出的解决方案中最可取的解决方案,直到Microsoft从他们那端解决问题为止。

英文:

If all of your nodes have the same IP address as the hostname (as is the case in Azure Cache for Redis, I think), then this is one way you could configure your client to map unresolved IP addresses back to the hostname listed in the certificate.

    @Bean
ClientResources clientResources(RedisProperties redisProperties) throws UnknownHostException {
var clientResourcesBuilder = DefaultClientResources.builder();
var configuredHost = redisProperties.getHost();
var inetAddresses = Arrays.asList(InetAddress.getAllByName(configuredHost));
MappingSocketAddressResolver resolver = MappingSocketAddressResolver.create(
DnsResolvers.UNRESOLVED,
hostAndPort -&gt; inetAddresses.stream()
.anyMatch(i -&gt; i.getHostAddress().equals(hostAndPort.getHostText())) ?
HostAndPort.of(configuredHost, hostAndPort.getPort()) :
hostAndPort
);
clientResourcesBuilder.socketAddressResolver(resolver);
return clientResourcesBuilder.build();
}

As best I understand it, this is the most preferable of the solutions listed on the Github issue until Microsoft fixes things from their end.

答案2

得分: 1

与独立模式下的连接不同,连接到 Azure Redis 的集群模式是一个分为两步的过程:

  1. 连接到 &lt;hostname:6380&gt;,进行身份验证,并获取集群终结点的详细信息。
  2. 连接到你在集群终结点详细信息中获得的 &lt;ip address:port&gt;,再次进行身份验证,然后向包含你的键的特定集群分片发送命令。

你之所以收到 No subject alternative names matching IP address &lt;redacted&gt; found 是因为 Azure Redis 在集群终结点详细信息中提供了一个 IP 地址 + 端口号,然后 Lettuce 尝试针对 IP 地址而不是主机名验证你的 SSL 连接,但因为它试图验证 SSL 证书的主题或 SAN something.redis.cache.windows.net 与你当前正在连接的服务器终结点 &lt;ip address&gt;:&lt;port&gt; 不匹配而失败。

在大多数客户端库中,您可以通过配置或覆盖 SSL 证书验证来验证服务器证书与您特定的 Redis 缓存主机名匹配来解决此问题。

例如,在 .Net 的 StackExchange.redis 中,有一个名为 sslhost 的配置设置,可用于此目的。

希望 Lettuce 也有类似的功能。

英文:

Unlike connecting in standalone mode, connecting to Azure redis in cluster mode is a two step process:

  1. Connect to &lt;hostname:6380&gt;, authenticate, and fetch the cluster endpoint details
  2. Connect to &lt;ip address:port&gt; that you got in the cluster endpoint details, authenticate again, and then send commands to the particular cluster shard your key is on

The reason you get No subject alternative names matching IP address &lt;redacted&gt; found is that Azure redis gives you an IP address + port number in the cluster endpoint details, and then Lettuce tries to validate your SSL connection against the IP address - instead of the hostname, but fails because its trying to verify the the SSL cert subject or SAN something.redis.cache.windows.net against the server endpoint that you are currently connecting to &lt;ip address&gt;:&lt;port&gt;.

You can get around this in most client libraries by configuring or overriding the SSL certificate validation to validate the server cert against your particular redis cache's hostname.

E.g. in .Net StackExchange.redis there's a config setting called 'sslhost' useful for this purpose.

Hopefully Lettuce has an equivalent.

huangapple
  • 本文由 发表于 2020年10月10日 05:16:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/64287424.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定