【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

 



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

相关文章
|
存储 算法 安全
oss加密方式
阿里云OSS提供多种数据加密选项增强安全性:SSE-S3和SSE-KMS(服务器端加密),其中SSE-KMS支持使用KMS托管密钥;SSE-C和CSE-KMS(客户端加密)允许用户自管加密密钥,CSE-KMS结合KMS增强安全;还有SSE-OSS,完全托管的加密方式,使用AES-256并定期轮转主密钥。这些方案满足不同用户对密钥管理和数据安全的需求。
608 2
|
运维 安全 网络安全
Flink CDC产品常见问题之flink1.18同步mysql-starrocks pipeline时报错如何解决
Flink CDC(Change Data Capture)是一个基于Apache Flink的实时数据变更捕获库,用于实现数据库的实时同步和变更流的处理;在本汇总中,我们组织了关于Flink CDC产品在实践中用户经常提出的问题及其解答,目的是辅助用户更好地理解和应用这一技术,优化实时数据处理流程。
|
3月前
|
数据采集 人工智能 物联网
教AI学会说'我是小喵'竟然这么神奇?LlamaFactory微调揭秘
想让AI助手记住自己叫什么名字?就像教小孩背诵身份证信息一样简单!通过LlamaFactory的SFT微调,你的AI不仅能记住自己是谁,还能在千万个问题中准确回答身份信息。从技术小白到微调高手,一篇文章搞定! #人工智能 #LlamaFactory #模型微调 #AI助手
370 2
|
3月前
|
弹性计算 应用服务中间件 测试技术
阿里云38元一年大家抢到了吗?轻量应用服务器200M带宽购买攻略
阿里云38元一年服务器抢购攻略:先注册阿里云新账号、完成实名认证,200M轻量应用服务器不限流量,每天抢购2次10:00和15:00,定好闹钟,重点来了地域选择后不能修改,但是镜像随便选就行,因为购买后还可以免费修改,所以手速要快,不要纠结配置的选择
1057 5
|
人工智能 自然语言处理 Java
对话即服务:Spring Boot整合MCP让你的CRUD系统秒变AI助手
本文介绍了如何通过Model Context Protocol (MCP) 协议将传统Spring Boot服务改造为支持AI交互的智能系统。MCP作为“万能适配器”,让AI以统一方式与多种服务和数据源交互,降低开发复杂度。文章以图书管理服务为例,详细说明了引入依赖、配置MCP服务器、改造服务方法(注解方式或函数Bean方式)及接口测试的全流程。最终实现用户通过自然语言查询数据库的功能,展示了MCP在简化AI集成、提升系统易用性方面的价值。未来,“对话即服务”有望成为主流开发范式。
9121 7
|
Java Maven Spring
springboot学习一:idea社区版本创建springboot项目的三种方式(第三种为主)
这篇文章介绍了在IntelliJ IDEA社区版中创建Spring Boot项目的三种方法,特别强调了第三种方法的详细步骤。
15560 0
springboot学习一:idea社区版本创建springboot项目的三种方式(第三种为主)
|
人工智能 自然语言处理 Java
Spring AI,Spring团队开发的新组件,Java工程师快来一起体验吧
文章介绍了Spring AI,这是Spring团队开发的新组件,旨在为Java开发者提供易于集成的人工智能API,包括机器学习、自然语言处理和图像识别等功能,并通过实际代码示例展示了如何快速集成和使用这些AI技术。
11951 4
Spring AI,Spring团队开发的新组件,Java工程师快来一起体验吧
|
安全 Java API
Java根据URL获取文件内容的实现方法
此示例展示了如何安全、有效地根据URL获取文件内容。它不仅展现了处理网络资源的基本技巧,还体现了良好的异常处理实践。在实际开发中,根据项目需求,你可能还需要添加额外的功能,如设置连接超时、处理HTTP响应码等。
1205 4
|
jenkins Java 持续交付
Exception in thread "main" java.lang.IllegalArgumentException: U+6570 ('.notdef') is not available in the font Helvetica-Bold, encoding: WinAnsiEncoding 问题解决
【5月更文挑战第26天】Exception in thread "main" java.lang.IllegalArgumentException: U+6570 ('.notdef') is not available in the font Helvetica-Bold, encoding: WinAnsiEncoding 问题解决
1242 2
下一篇
开通oss服务