問題描述

使用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。

運行時的錯誤截圖

【Azure Redis】客户端應用使用 Azure Redis Cluster 報錯 No subject alternative names matching IP address ..._redis

示例代碼

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 versinotallow="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指令!

【Azure Redis】客户端應用使用 Azure Redis Cluster 報錯 No subject alternative names matching IP address ..._IP_02

修改後完整的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模型解讀以上代碼後,分析出來的代碼流程圖

【Azure Redis】客户端應用使用 Azure Redis Cluster 報錯 No subject alternative names matching IP address ..._IP_03

 

 

 

參考資料

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

 



當在複雜的環境中面臨問題,格物之道需:濁而靜之徐清,安以動之徐生。 雲中,恰是如此!