【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

 



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

相关文章
|
14天前
|
数据采集 人工智能 运维
AgentRun 实战:快速构建 AI 舆情实时分析专家
搭建“舆情分析专家”,函数计算 AgentRun 快速实现从数据采集到报告生成全自动化 Agent。
632 55
|
1月前
|
存储 SQL JSON
打通可观测性的“任督二脉”:实体与关系的终极融合
阿里云推出图查询能力,基于 graph-match、graph-call、Cypher 三重引擎,实现服务依赖、故障影响、权限链路的秒级可视化与自动化分析,让可观测从‘看板时代’迈向‘图谱时代’。
262 49
|
1月前
|
人工智能 运维 Serverless
一杯咖啡成本搞定多模态微调:FC DevPod + Llama-Factory 极速实战
告别显存不足、环境配置难、成本高昂的微调困境!基于阿里云函数计算FC与Llama-Factory,5分钟搭建微调流水线,一键完成多模态模型的微调。
274 20
|
15天前
|
监控 安全 Unix
iOS 崩溃排查不再靠猜!这份分层捕获指南请收好
从 Mach 内核异常到 NSException,从堆栈遍历到僵尸对象检测,阿里云 RUM iOS SDK 基于 KSCrash 构建了一套完整、异步安全、生产可用的崩溃捕获体系,让每一个线上崩溃都能被精准定位。
313 48
|
4天前
|
存储 弹性计算 人工智能
2026 年阿里云服务器租用价格全解析:年付、月付收费标准与配置参考
阿里云服务器租用价格受实例类型、配置、计费周期影响,从低至 38 元 / 年的轻量机型到数万元 / 年的高性能实例不等。以下结合 2026 年最新收费标准,梳理轻量应用服务器、ECS 云服务器及 GPU 服务器的核心配置与多周期价格,覆盖年付、3 年付、月付及按量付费场景,帮助用户按需选择。
96 11
|
13天前
|
供应链 容器
什么是code128码?
Code 128码是一种高密度条形码,支持全ASCII字符,广泛用于物流、运输和供应链管理。它分为A、B、C三个子集,可编码字母、数字及控制符,具有高密度、小空间优势,适用于复杂数据编码需求。
339 3
|
9天前
|
人工智能 安全 API
Nacos 安全护栏:MCP、Agent、配置全维防护,重塑 AI Registry 安全边界
Nacos安全新标杆:精细鉴权、无感灰度、全量审计!
244 37
|
4天前
|
弹性计算 应用服务中间件 测试技术
阿里云38元一年大家抢到了吗?轻量应用服务器200M带宽购买攻略
阿里云38元一年服务器抢购攻略:先注册阿里云新账号、完成实名认证,200M轻量应用服务器不限流量,每天抢购2次10:00和15:00,定好闹钟,重点来了地域选择后不能修改,但是镜像随便选就行,因为购买后还可以免费修改,所以手速要快,不要纠结配置的选择
86 5
|
24天前
|
SQL HIVE
十一、Hive JOIN 连接查询
在 Hive 的世界里,JOIN 就像是数据间的红线,把原本分散在各自表里的信息串联起来。无论是内连接、外连接,还是 Hive 特有的左半连接,都各有“武功招式”,适用于不同场景。
127 12
|
14天前
|
存储 缓存 NoSQL
即将开源 | 阿里云 Tair KVCache Manager:企业级全局 KVCache 管理服务的架构设计与实现
阿里云 Tair 联合团队推出企业级全局 KVCache 管理服务 Tair KVCache Manager,通过中心化元数据管理与多后端存储池化,实现 KVCache 的跨实例共享与智能调度。该服务解耦算力与存储,支持弹性伸缩、多租户隔离及高可用保障,显著提升缓存命中率与资源利用率,重构大模型推理成本模型,支撑智能体时代的规模化推理需求。