【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

 



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

相关文章
|
3天前
|
存储 JavaScript 前端开发
JavaScript基础
本节讲解JavaScript基础核心知识:涵盖值类型与引用类型区别、typeof检测类型及局限性、===与==差异及应用场景、内置函数与对象、原型链五规则、属性查找机制、instanceof原理,以及this指向和箭头函数中this的绑定时机。重点突出类型判断、原型继承与this机制,助力深入理解JS面向对象机制。(238字)
|
2天前
|
云安全 人工智能 安全
阿里云2026云上安全健康体检正式开启
新年启程,来为云上环境做一次“深度体检”
1474 6
|
4天前
|
安全 数据可视化 网络安全
安全无小事|阿里云先知众测,为企业筑牢防线
专为企业打造的漏洞信息收集平台
1311 2
|
3天前
|
缓存 算法 关系型数据库
深入浅出分布式 ID 生成方案:从原理到业界主流实现
本文深入探讨分布式ID的生成原理与主流解决方案,解析百度UidGenerator、滴滴TinyID及美团Leaf的核心设计,涵盖Snowflake算法、号段模式与双Buffer优化,助你掌握高并发下全局唯一ID的实现精髓。
324 160
|
3天前
|
人工智能 自然语言处理 API
n8n:流程自动化、智能化利器
流程自动化助你在重复的业务流程中节省时间,可通过自然语言直接创建工作流啦。
374 4
n8n:流程自动化、智能化利器
|
12天前
|
机器学习/深度学习 安全 API
MAI-UI 开源:通用 GUI 智能体基座登顶 SOTA!
MAI-UI是通义实验室推出的全尺寸GUI智能体基座模型,原生集成用户交互、MCP工具调用与端云协同能力。支持跨App操作、模糊语义理解与主动提问澄清,通过大规模在线强化学习实现复杂任务自动化,在出行、办公等高频场景中表现卓越,已登顶ScreenSpot-Pro、MobileWorld等多项SOTA评测。
1492 7
|
5天前
|
人工智能 API 开发工具
Skills比MCP更重要?更省钱的多!Python大佬这观点老金测了一周终于懂了
加我进AI学习群,公众号右下角“联系方式”。文末有老金开源知识库·全免费。本文详解Claude Skills为何比MCP更轻量高效:极简配置、按需加载、省90% token,适合多数场景。MCP仍适用于复杂集成,但日常任务首选Skills。推荐先用SKILL.md解决,再考虑协议。附实测对比与配置建议,助你提升效率,节省精力。关注老金,一起玩转AI工具。
|
2天前
|
Linux 数据库
Linux 环境 Polardb-X 数据库 单机版 rpm 包 安装教程
本文介绍在CentOS 7.9环境下安装PolarDB-X单机版数据库的完整流程,涵盖系统环境准备、本地Yum源配置、RPM包安装、用户与目录初始化、依赖库解决、数据库启动及客户端连接等步骤,助您快速部署运行PolarDB-X。
230 1
Linux 环境 Polardb-X 数据库 单机版 rpm 包 安装教程
|
13天前
|
人工智能 Rust 运维
这个神器让你白嫖ClaudeOpus 4.5,Gemini 3!还能接Claude Code等任意平台
加我进AI讨论学习群,公众号右下角“联系方式”文末有老金的 开源知识库地址·全免费
1362 17
|
3天前
|
自然语言处理 监控 测试技术
互联网大厂“黑话”完全破译指南
互联网大厂黑话太多听不懂?本文整理了一份“保姆级”职场黑话词典,涵盖PRD、A/B测试、WLB、埋点、灰度发布等高频术语,用大白话+生活化类比,帮你快速听懂同事在聊什么。非技术岗也能轻松理解,建议收藏防踩坑。
285 161