【Azure Redis】客户端应用使用 Azure Redis Cluster 报错 java.security.cert.CertificateException: No subject alternative names matching IP address xxx.xxx.xxx.xxx found

简介: 使用Lettuce连接Azure Redis集群时,因SSL证书仅含域名不支持IP地址,导致“CertificateException”错误。通过自定义`MappingSocketAddressResolver`,将IP映射为域名进行证书验证,结合`ClientResources`配置实现安全连接,最终成功访问Redis集群并执行操作。

问题描述

使用Azure Cache for Redis的集群模式。应用客户端为Java代码,使用Lettuce 作为Redis 客户端SDK。启动项目报错:Caused by: java.security.cert.CertificateException: No subject alternative names matching IP address 159.27.xxx.xxx found。

运行时的错误截图

示例代码

package com.lbazureredis;
import io.lettuce.core.RedisURI;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
public class Main {
    public static void main(String[] args) {
        
        System.out.println("Hello world! This is Redis Cluster example.");
        
        RedisURI redisUri = RedisURI.Builder.redis("<yourredisname>.redis.cache.chinacloudapi.cn", 6380)
                .withPassword("<your redis access key>").withSsl(true).build();
        RedisClusterClient clusterClient = RedisClusterClient.create(redisUri);
        StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();
        RedisAdvancedClusterCommands<String, String> syncCommands = connection
                .sync();
        String pingResponse = syncCommands.ping();
        System.out.println("Ping response: " + pingResponse);
        syncCommands.set("mykey", "Hello, Redis Cluster!");
        String value = syncCommands.get("mykey");
        System.out.println("Retrieved value: " + value);
        
        connection.close();
        clusterClient.shutdown();
    }
}

项目POM.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.lbazureredis</groupId>
    <artifactId>test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
    <dependencies>
        <!-- Lettuce Redis Client -->
        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>6.3.1.RELEASE</version>
        </dependency>
        <!-- SLF4J for logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.9</version>
        </dependency>
    </dependencies>
</project>


针对以上问题,如何解决呢?

 

问题解答

根据错误信息搜索后,得到Azure官方最佳实践文档中的解答:https://github.com/Azure/AzureCacheForRedis/blob/main/Lettuce%20Best%20Practices.md

The reason this is required is because SSL certification validates the address of the Redis Nodes with the SAN (Subject Alternative Names) in the SSL certificate. Redis protocol requires that these node addresses should be IP addresses. However, the SANs in the Azure Redis SSL certificates contains only the Hostname since Public IP addresses can change and as a result not completely secure.

在Redis Protocol验证中,必须验证证书中包含IP地址,但由于Azure Redis部署在云环境中,IP地址是不固定的。所以默认情况下,Redis SSL证书中包含的是域名。为了解决这个问题,需要建立一个Host与IP地址的映射关系,使得Lettuce客户端在验证Redis证书时通过域名验证而非IP地址,用于解决No subject alternative names matching IP address 159.27.xxx.xxx found 问题


参考文档中的方法,自定义MappingSocketAddressResolver


        Function<HostAndPort, HostAndPort> mappingFunction = new Function<HostAndPort, HostAndPort>() {
            @Override
            public HostAndPort apply(HostAndPort hostAndPort) {
                String cacheIP = "";
                try {
                    InetAddress[] addresses = DnsResolvers.JVM_DEFAULT.resolve(host);
                    cacheIP = addresses[0].getHostAddress();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                HostAndPort finalAddress = hostAndPort;
                if (hostAndPort.hostText.equals(cacheIP))
                    finalAddress = HostAndPort.of(host, hostAndPort.getPort());
                return finalAddress;
            }
        };
        MappingSocketAddressResolver resolver = MappingSocketAddressResolver.create(DnsResolvers.JVM_DEFAULT,
                mappingFunction);
        ClientResources res = DefaultClientResources.builder()
                .socketAddressResolver(resolver).build();
        RedisURI redisURI = RedisURI.Builder.redis(host).withSsl(true)
                .withPassword(password)
                .withClientName("LettuceClient")
                .withPort(6380)
                .build();
        RedisClusterClient redisClient = RedisClusterClient.create(res, redisURI);


代码解读

mappingFunction

  • 它是一个自定义的地址映射逻辑,用于处理 Lettuce 在连接 Redis 集群时的主机名与 IP 地址问题。
  • 它通过 DnsResolvers.JVM_DEFAULT 对指定的域名进行 DNS 解析,获取对应的 IP 地址。如果当前 HostAndPort 的 hostText 与解析出的 IP 相同,则将其替换为原始域名 host,保持端口不变。
  • 这一逻辑的核心目的是解决 SSL 证书校验问题,因为证书通常绑定域名而非 IP,确保连接时使用域名进行验证,避免因 IP 导致的握手失败。

MappingSocketAddressResolver

  • 它是 Lettuce 提供的一个工具类,用于在连接 Redis 时插入自定义的地址解析逻辑。
  • 它结合默认的 DNS 解析器和 mappingFunction,在每次解析 Socket 地址时执行映射操作。
  • 通过这种方式,客户端可以在 DNS 解析后对结果进行二次处理,例如将 IP 地址重新映射为域名。
  • 这对于云服务场景(如 Azure Redis)非常重要,因为这些服务的 SSL 证书通常只对域名有效,而不是 IP 地址。

DefaultClientResources

  • 作为 Lettuce 的核心资源管理器,用于配置客户端的底层行为,包括线程池、DNS 解析器、事件循环等。在这里,它的作用是将自定义的 MappingSocketAddressResolver 注入到客户端资源中,使所有连接请求都遵循自定义的地址解析逻辑。
  • 通过这种方式,整个 Lettuce 客户端在连接 Redis 集群时都会使用域名而非 IP,确保 SSL 校验通过,同时保持连接的稳定性和安全性。

 

执行结果

再次运行,成功连接到Azure Redis Cluster 及执行Ping, Set, Get指令!

修改后完整的Java示例代码如下:

package com.lbazureredis;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.function.Function;
import io.lettuce.core.RedisURI;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import io.lettuce.core.internal.HostAndPort;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;
import io.lettuce.core.resource.DnsResolvers;
import io.lettuce.core.resource.MappingSocketAddressResolver;
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello world! This is Redis Cluster example.");
        String host = "<yourredisname>.redis.cache.chinacloudapi.cn";
        String password = "<your redis access key>";
        Function<HostAndPort, HostAndPort> mappingFunction = new Function<HostAndPort, HostAndPort>() {
            @Override
            public HostAndPort apply(HostAndPort hostAndPort) {
                String cacheIP = "";
                try {
                    InetAddress[] addresses = DnsResolvers.JVM_DEFAULT.resolve(host);
                    cacheIP = addresses[0].getHostAddress();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                HostAndPort finalAddress = hostAndPort;
                if (hostAndPort.hostText.equals(cacheIP))
                    finalAddress = HostAndPort.of(host, hostAndPort.getPort());
                return finalAddress;
            }
        };
        MappingSocketAddressResolver resolver = MappingSocketAddressResolver.create(DnsResolvers.JVM_DEFAULT,
                mappingFunction);
        ClientResources res = DefaultClientResources.builder()
                .socketAddressResolver(resolver).build();
        RedisURI redisURI = RedisURI.Builder.redis(host).withSsl(true)
                .withPassword(password)
                .withClientName("LettuceClient")
                .withPort(6380)
                .build();
        RedisClusterClient redisClient = RedisClusterClient.create(res, redisURI);
        // Cluster specific settings for optimal reliability.
        ClusterTopologyRefreshOptions refreshOptions = ClusterTopologyRefreshOptions.builder()
                .enablePeriodicRefresh(Duration.ofSeconds(5))
                .dynamicRefreshSources(false)
                .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(5))
                .enableAllAdaptiveRefreshTriggers().build();
        redisClient.setOptions(ClusterClientOptions.builder()
                .socketOptions(SocketOptions.builder()
                        .keepAlive(true)
                        .build())
                .topologyRefreshOptions(refreshOptions).build());
                
        StatefulRedisClusterConnection<String, String> connection = redisClient.connect();
        RedisAdvancedClusterCommands<String, String> syncCommands = connection.sync();
        RedisAdvancedClusterAsyncCommands<String, String> asyncCommands = connection.async();
        String pingResponse = syncCommands.ping();
        System.out.println("Ping response: " + pingResponse);
        syncCommands.set("mykey", "Hello, Redis Cluster!");
        String value = syncCommands.get("mykey");
        System.out.println("Retrieved value: " + value);
        connection.close();
        redisClient.shutdown();
    }
}


代码流程图

基于AI模型解读以上代码后,分析出来的代码流程图

 

 

 

参考资料

Best Practices for using Azure Cache for Redis with Lettuce :https://github.com/Azure/AzureCacheForRedis/blob/main/Lettuce%20Best%20Practices.md

 



当在复杂的环境中面临问题,格物之道需:浊而静之徐清,安以动之徐生。 云中,恰是如此!

相关文章
|
NoSQL 网络协议 数据库
为什么 Lettuce 会带来更长的故障时间
本文详述了阿里云数据库 Tair/Redis 将使用长连接客户端在非预期故障宕机切换场景下的恢复时间从最初的 900s 降到 120s 再到 30s的优化过程,涉及产品优化,开源产品问题修复等诸多方面。
71324 11
为什么 Lettuce 会带来更长的故障时间
|
存储 安全 Java
一文带你搞懂OAuth2.0
一文带你搞懂OAuth2.0
628 0
|
存储 算法 安全
oss加密方式
阿里云OSS提供多种数据加密选项增强安全性:SSE-S3和SSE-KMS(服务器端加密),其中SSE-KMS支持使用KMS托管密钥;SSE-C和CSE-KMS(客户端加密)允许用户自管加密密钥,CSE-KMS结合KMS增强安全;还有SSE-OSS,完全托管的加密方式,使用AES-256并定期轮转主密钥。这些方案满足不同用户对密钥管理和数据安全的需求。
683 2
|
NoSQL 网络协议 Java
【Azure Redis】Redis服务端的故障转移(Failover)导致客户端应用出现15分钟超时问题的模拟及解决
在使用 Azure Cache for Redis 服务时,因服务端维护可能触发故障转移。Linux 环境下使用 Lettuce SDK 会遇到超时 15 分钟的已知问题。本文介绍如何通过重启 Primary 节点主动复现故障转移,并提供多种解决方案,包括调整 TCP 设置、升级 Lettuce 版本、配置 TCP_USER_TIMEOUT 及使用其他 SDK(如 Jedis)来规避此问题。
443 1
|
9月前
|
Java 开发工具
【Azure Storage Account】Java Code访问Storage Account File Share的上传和下载代码示例
本文介绍如何使用Java通过azure-storage-file-share SDK实现Azure文件共享的上传下载。包含依赖引入、客户端创建及完整示例代码,助你快速集成Azure File Share功能。
606 6
|
6月前
|
SQL 人工智能 自然语言处理
重构数据处理流程,实现从手动到AI赋能的智能化跃迁
在企业数字化进程中,数据处理常受限于技术门槛与人工低效。JBoltAI4系列通过AI实现结构化、非结构化及知识图谱数据的智能处理:支持自然语言查数据库、自动解析文档音视频、AI构建知识图谱,并打通数据接入、处理到应用的端到端闭环,让数据高效转化为业务资产,推动企业从“人力驱动”迈向“智能驱动”。
297 3
|
6月前
|
数据采集 人工智能 物联网
教AI学会说'我是小喵'竟然这么神奇?LlamaFactory微调揭秘
想让AI助手记住自己叫什么名字?就像教小孩背诵身份证信息一样简单!通过LlamaFactory的SFT微调,你的AI不仅能记住自己是谁,还能在千万个问题中准确回答身份信息。从技术小白到微调高手,一篇文章搞定! #人工智能 #LlamaFactory #模型微调 #AI助手
536 2
|
6月前
|
弹性计算 应用服务中间件 测试技术
阿里云38元一年大家抢到了吗?轻量应用服务器200M带宽购买攻略
阿里云38元一年服务器抢购攻略:先注册阿里云新账号、完成实名认证,200M轻量应用服务器不限流量,每天抢购2次10:00和15:00,定好闹钟,重点来了地域选择后不能修改,但是镜像随便选就行,因为购买后还可以免费修改,所以手速要快,不要纠结配置的选择
1723 5

热门文章

最新文章