全部產品
Search
文件中心

Tair (Redis® OSS-Compatible):啟用TLS(SSL)加密串連執行個體

更新時間:Nov 05, 2024

在使用用戶端程式串連Tair (Redis OSS-compatible)執行個體時,您可以啟用TLS(SSL)加密功能提高資料鏈路的安全性,保障資料的完整性。您可以根據自身應用特點選用任何相容Redis協議的用戶端程式。本文列舉一些常見的用戶端程式的程式碼範例,協助您快速串連。

前提條件

  • 執行個體已開啟TLS(SSL)加密,具體操作參見開啟TLS加密

  • 用戶端部署在與執行個體相同Virtual Private Cloud的ECS執行個體上。

注意事項

若執行個體開啟了專用網路免密訪問,同一專用網路下的用戶端程式無需設定密碼即可串連執行個體。

準備工作

  1. 將用戶端所在的ECS內網IP地址添加至執行個體的白名單中,具體操作請參見設定白名單

  2. 擷取以下資訊並設定在用戶端程式的代碼中:

    需擷取的資訊

    擷取方式

    執行個體的串連地址

    執行個體支援多種串連地址,推薦使用專用網路串連,可獲得更高的安全性和更低的網路延遲。更多資訊,請參見查看串連地址

    連接埠號碼

    連接埠號碼預設為6379,您也可以自訂連接埠號碼。具體操作,請參見修改串連地址或連接埠

    執行個體的帳號(部分用戶端程式無需設定)

    執行個體預設會建立一個以執行個體ID命名的帳號(例如r-bp10noxlhcoim2****),您也可以建立一個新的帳號並賦予許可權。更多資訊,請參見建立與管理帳號

    帳號的密碼

    根據選取帳號的不同,密碼的填寫格式有一定區別:

    • 預設帳號(以執行個體ID命名的帳號):直接填寫密碼即可。

    • 新建立的帳號:密碼格式為<user>:<password>。例如自訂帳號為testaccount,密碼為Rp829dlwa,密碼需填寫為testaccount:Rp829dlwa

    說明
    • 如果通過第三方資料庫管理工具(例如RDM等)串連執行個體,請在密碼框中輸入user:password進行串連。

    • 如果忘記密碼,您可以重設密碼。具體操作,請參見修改或重設密碼

  3. 下載CA認證,請參見開啟TLS加密

代理串連模式

此模式適用於標準架構、叢集架構代理模式、讀寫分離架構執行個體,請按需展開以下範例程式碼。

redis-cli

您需要在編譯Redis時,指定BUILD_TLS=yes,才能在redis-cli中啟用TLS串連執行個體。

  1. 登入ECS執行個體,下載、安裝redis-cli。

    1. 執行下述命令,安裝相關依賴。

      yum install openssl-devel gcc
    2. 執行下述命令下載Redis源碼檔案。

      wget https://download.redis.io/releases/redis-7.0.0.tar.gz
      說明

      本文以redis-7.0.0版本為例示範操作流程,您也可以安裝其他版本。具體操作,請參見Redis官網

    3. 執行下述命令解壓Redis源碼檔案。

      tar xzf redis-7.0.0.tar.gz
    4. 執行下述命令進入解壓後的目錄,編譯安裝Redis源碼檔案並開啟TLS。

      cd redis-7.0.0&&make BUILD_TLS=yes

      編譯安裝需要一段時間(通常為2分鐘~3分鐘)。

  2. 在命令列視窗執行下述命令串連執行個體。

    ./src/redis-cli -h r-bp14joyeihew30****.redis.rds.aliyuncs.com -p 6379 --tls --cacert ./ApsaraDB-CA-Chain.pem

    cacert參數後需指定CA認證的路徑。

  3. 執行下述命令完成密碼驗證。

    AUTH password

    顯示OK表示已成功串連執行個體。

Java

本樣本以Jedis 3.6.0版本為例,推薦使用最新版本。

import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class JedisSSLTest {
    private static SSLSocketFactory createTrustStoreSSLSocketFactory(String jksFile) throws Exception {
        KeyStore trustStore = KeyStore.getInstance("jks");
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream(jksFile);
            trustStore.load(inputStream, null);
        } finally {
            inputStream.close();
        }

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
        trustManagerFactory.init(trustStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, new SecureRandom());
        return sslContext.getSocketFactory();
    }

    public static void main(String[] args) throws Exception {
        // ApsaraDB-CA-Chain.jks為認證檔案名稱。
        final SSLSocketFactory sslSocketFactory = createTrustStoreSSLSocketFactory("ApsaraDB-CA-Chain.jks");
        // 串連池的設定分別為執行個體的串連地址、連接埠號碼、逾時設定、密碼。
        JedisPool pool = new JedisPool(new GenericObjectPoolConfig(), "r-bp1zxszhcgatnx****.redis.rds.aliyuncs.com",
            6379, 2000, "redistest:Pas***23", 0, true, sslSocketFactory, null, null);

        try (Jedis jedis = pool.getResource()) {
            jedis.set("key", "value");
            System.out.println(jedis.get("key"));
        }
    }
}

Python

本樣本以redis-py用戶端為例,推薦使用最新版本。

串連池串連

#!/bin/python
import redis

# 設定串連池,分別將host、port、password的值分別替換為執行個體的串連地址、連接埠號碼、密碼。
# ApsaraDB-CA-Chain.pem為認證檔案名稱。
pool = redis.ConnectionPool(connection_class=redis.connection.SSLConnection, max_connections=100,
                            host="r-bp1zxszhcgatnx****.redis.rds.aliyuncs.com", port=6379, password="redistest:Pas***23",
                            ssl_cert_reqs=True, ssl_ca_certs="ApsaraDB-CA-Chain.pem")
client = redis.Redis(connection_pool=pool)
client.set("hi", "redis")
print client.get("hi")

普通串連

#!/bin/python
import redis

# 設定串連資訊,分別將host、port、password的值分別替換為執行個體的串連地址、連接埠號碼、密碼。
# ApsaraDB-CA-Chain.pem為認證檔案名稱。
client = redis.Redis(host="r-bp1zxszhcgatnx****.redis.rds.aliyuncs.com", port=6379,
                     password="redistest:Test1234", ssl=True,
                    ssl_cert_reqs="required", ssl_ca_certs="ApsaraDB-CA-Chain.pem")

client.set("hello", "world")
print client.get("hello")

PHP

本樣本以predis用戶端為例,推薦使用最新版本。若您使用的是phpredis用戶端,串連樣本請參見此issue

<?php

require __DIR__.'/predis/autoload.php';

/* 設定串連資訊,分別將host、port、password的值分別替換為執行個體的串連地址、連接埠號碼、密碼
ApsaraDB-CA-Chain.pem為認證檔案名稱*/
$client = new Predis\Client([
    'scheme' => 'tls',
    'host'   => 'r-bp1zxszhcgatnx****.redis.rds.aliyuncs.com',
    'port'   => 6379,
    'password' => 'redistest:Pas***23',
    'ssl'    => ['cafile' => 'ApsaraDB-CA-Chain.pem', 'verify_peer' => true],
]);
/* 依次替換下述代碼中的串連地址和連接埠 */
//$client = new Predis\Client('tls://r-bp1zxszhcgatnx****.redis.rds.aliyuncs.com:6379?ssl[cafile]=ApsaraDB-CA-Chain.pem&ssl[verify_peer]=1');

$client->set("hello", "world");
print $client->get("hello")."\n";

?>

C#

本樣本以StackExchange.Redis用戶端為例,推薦使用最新版本。

using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using StackExchange.Redis;

namespace SSLTest
{
    class Program
    {
        private static bool CheckServerCertificate(object sender, X509Certificate certificate,
            X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            var ca = new X509Certificate2(
                "/your path/ApsaraDB-CA-Chain/ApsaraDB-CA-Chain.pem");
            return chain.ChainElements
                .Cast<X509ChainElement>()
                .Any(x => x.Certificate.Thumbprint == ca.Thumbprint);
        }

        static void Main(string[] args)
        {
          // 設定串連資訊,分別將host、port、password的值分別替換為執行個體的串連地址、連接埠號碼、密碼。
          // ApsaraDB-CA-Chain.pem為認證檔案名稱。
            ConfigurationOptions config = new ConfigurationOptions()
            {
                EndPoints = {"r-bp10q23zyfriodu*****.redis.rds.aliyuncs.com:6379"},
                Password = "redistest:Pas***23",
                Ssl = true,
            };

            config.CertificateValidation += CheckServerCertificate;
            using (var conn = ConnectionMultiplexer.Connect(config))
            {
                Console.WriteLine("connected");
                var db = conn.GetDatabase();
                db.StringSet("hello", "world");
                Console.WriteLine(db.StringGet("hello"));
            }
        }
    }
}

Spring Data Redis

本樣本以Spring Data Redis 2.7.12版(Java 1.8版本)為例,推薦使用最新版本。

@Configuration
public class RedisConfig {
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 建議您將TLS認證配置存放在properties檔案中。
        String host = "r-bp1zxszhcgatnx****.redis.rds.aliyuncs.com";
        int port = 6379;
        String password = "Pas***23";
        String trustStoreFilePath = "/path/to/ApsaraDB-CA-Chain.jks";

        ClientOptions clientOptions = ClientOptions.builder().sslOptions(
            SslOptions.builder().jdkSslProvider().truststore(new File(trustStoreFilePath)).build()).build();
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(host);
        config.setPort(port);
        config.setPassword(password);
        LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder()
            .clientOptions(clientOptions)
            .useSsl().build();
        return new LettuceConnectionFactory(config, lettuceClientConfiguration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    }
}

Lettuce

本樣本以Lettuce 6.2.4.RELEASE版本為例,推薦使用最新版本。

public class SSLExample {
    public static void main(String[] args) throws Exception {
        String host = "r-bp1zxszhcgatnx****.redis.rds.aliyuncs.com";
        int port = 6379;
        String password = "Pas***23";
        String trustStoreFilePath = "/path/to/ApsaraDB-CA-Chain.jks";

        RedisURI uri = RedisURI.builder()
            .withHost(host)
            .withPort(port)
            .withPassword(password.toCharArray())
            .withSsl(true).build();

        SslOptions sslOptions = SslOptions.builder()
            .jdkSslProvider()
            .truststore(new File(trustStoreFilePath)).build();

        ClientOptions clientOptions = ClientOptions.builder()
            .sslOptions(sslOptions).build();
        RedisClient client = RedisClient.create(uri);
        client.setOptions(clientOptions);

        RedisCommands<String, String> sync = client.connect().sync();
        System.out.println(sync.set("key", "value"));
        System.out.println(sync.get("key"));

    }
}

Go

本樣本以go-redis v9.5.1用戶端為例,推薦使用v9.0以上版本。

package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"crypto/tls"
	"crypto/x509"
	"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func main() {
        caCert, err := ioutil.ReadFile("/root/ApsaraDB-CA-Chain.pem")
	if err != nil {
		fmt.Println("Error loading CA certificate:", err)
		return
	}

	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)

	tlsConfig := &tls.Config{
		RootCAs:            caCertPool,
		InsecureSkipVerify: true, // Not actually skipping, we check the cert in VerifyPeerCertificate
		VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
			// Code copy/pasted and adapted from
			// https://github.com/golang/go/blob/81555cb4f3521b53f9de4ce15f64b77cc9df61b9/src/crypto/tls/handshake_client.go#L327-L344, but adapted to skip the hostname verification.
			// See https://github.com/golang/go/issues/21971#issuecomment-412836078.

			// If this is the first handshake on a connection, process and
			// (optionally) verify the server's certificates.
			certs := make([]*x509.Certificate, len(rawCerts))
			for i, asn1Data := range rawCerts {
				cert, err := x509.ParseCertificate(asn1Data)
				if err != nil {
					panic(err)
				}
				certs[i] = cert
			}

			opts := x509.VerifyOptions{
				Roots:         caCertPool,
				DNSName:       "", // <- skip hostname verification
				Intermediates: x509.NewCertPool(),
			}

			for i, cert := range certs {
				if i == 0 {
					continue
				}
				opts.Intermediates.AddCert(cert)
			}
			_, err := certs[0].Verify(opts)
			return err
		},
	}

	rdb := redis.NewClient(&redis.Options{
		Addr:    "r-2zee50zxi5iiqm****.redis.rds-aliyun.rds.aliyuncs.com:6379",
		Username: "default",
		Password: "Pas***23",
		TLSConfig: tlsConfig,
	})

	err = rdb.Set(ctx, "key", "value", 0).Err()
	if err != nil {
		panic(err)
	}

	val, err := rdb.Get(ctx, "key").Result()
	if err != nil {
		panic(err)
	}
	fmt.Println("key:", val)
}

直連模式

此模式僅適用於叢集架構直連模式,請按需展開以下範例程式碼。

redis-cli

您需要在編譯Redis時,指定BUILD_TLS=yes,才能在redis-cli中啟用TLS串連執行個體。

  1. 登入ECS執行個體,下載、安裝redis-cli。

    1. 執行下述命令,安裝相關依賴。

      yum install openssl-devel gcc
    2. 執行下述命令下載Redis源碼檔案。

      wget https://download.redis.io/releases/redis-7.0.0.tar.gz
      說明

      本文以redis-7.0.0版本為例示範操作流程,您也可以安裝其他版本。具體操作,請參見Redis官網

    3. 執行下述命令解壓Redis源碼檔案。

      tar xzf redis-7.0.0.tar.gz
    4. 執行下述命令進入解壓後的目錄,編譯安裝Redis源碼檔案並開啟TLS。

      cd redis-7.0.0&&make BUILD_TLS=yes

      編譯安裝需要一段時間(通常為2分鐘~3分鐘)。

  2. 在命令列視窗執行下述命令串連執行個體。

    ./src/redis-cli -h r-bp14joyeihew30****.redis.rds.aliyuncs.com -p 6379 -c --tls --cacert ./ApsaraDB-CA-Chain.pem

    cacert參數後需指定CA認證的路徑。

  3. 執行下述命令完成密碼驗證。

    AUTH password

    顯示OK表示已成功串連執行個體。

Java

本樣本以Jedis 4.3.0版本為例,推薦使用最新版本。

import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.HashSet;
import java.util.Set;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import redis.clients.jedis.ConnectionPoolConfig;
import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;

public class JedisClusterTSL {
    private static final int DEFAULT_TIMEOUT = 2000;
    private static final int DEFAULT_REDIRECTIONS = 5;
    private static final ConnectionPoolConfig jedisPoolConfig = new ConnectionPoolConfig();

    private static SSLSocketFactory createTrustStoreSSLSocketFactory(String jksFile) throws Exception {
        KeyStore trustStore = KeyStore.getInstance("jks");
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream(jksFile);
            trustStore.load(inputStream, null);
        } finally {
            inputStream.close();
        }

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
        trustManagerFactory.init(trustStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, new SecureRandom());
        return sslContext.getSocketFactory();
    }

    public static void main(String args[]) throws Exception{
        // 最大空閑串連數,由於直連模式為用戶端直接連接某個資料庫分區,需要保證:業務機器數 * MaxTotal < 單個資料庫分區的最大串連數。
        jedisPoolConfig.setMaxTotal(30);
        // 最大空閑串連數, 根據業務需要設定。
        jedisPoolConfig.setMaxIdle(30);
        jedisPoolConfig.setMinIdle(15);

        // 直連地址。
        int port = 6379;
        String host = "r-2zee50zxi5iiq****.redis.rds-aliyun.rds.aliyuncs.com";
        String user = "default";
        String password = "Pas***23";

        final SSLSocketFactory sslSocketFactory = createTrustStoreSSLSocketFactory("/root/ApsaraDB-CA-Chain.jks");
        DefaultJedisClientConfig jedisClientConfig = DefaultJedisClientConfig.builder().connectionTimeoutMillis(DEFAULT_TIMEOUT)
            .socketTimeoutMillis(DEFAULT_TIMEOUT)
            .user(user).password(password)
            .ssl(true)
            .sslSocketFactory(sslSocketFactory).build();

        Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
        jedisClusterNode.add(new HostAndPort(host, port));
        JedisCluster jc = new JedisCluster(jedisClusterNode, jedisClientConfig, DEFAULT_REDIRECTIONS, jedisPoolConfig);

        System.out.println(jc.set("key", "value"));
        System.out.println(jc.get("key"));

        jc.close();     // 當應用退出,需銷毀資源時,調用此方法。此方法會中斷連線、釋放資源。
    }
}

Python

本樣本以redis-py 4.3.6(Python 3.6)用戶端為例,推薦使用最新版本。

#!/usr/bin/env python
from redis.cluster import RedisCluster

# 分別將host和port的值替換為執行個體的串連地址、連接埠號碼。
host = 'r-2zee50zxi5iiqm****.redis.rds-aliyun.rds.aliyuncs.com'
port = 6379
# 分別將user和pwd的值替換為執行個體的帳號和密碼。
user = 'default'
pwd = 'Pas***23'

rc = RedisCluster(host=host, port=port, username=user, password=pwd, ssl=True, ssl_ca_certs="/root/ApsaraDB-CA-Chain.pem")
# 串連建立後即可執行資料庫操作,下述代碼為您提供SET與GET的使用樣本。
rc.set('foo', 'bar')
print(rc.get('foo'))

PHP

本樣本以phpredis 5.3.7用戶端為例,推薦使用最新版本。

<?php
 // 直連地址和串連連接埠。
 $array = ['r-2zee50zxi5iiqm****.redis.rds-aliyun.rds.aliyuncs.com:6379'];
 // 串連密碼。
 $pwd = "Pas***23";
 // TLS串連資訊
 $tls = ["verify_peer" => false, "verify_peer_name" => false];
 // 使用密碼串連叢集。
 $obj_cluster = new RedisCluster(NULL, $array, 1.5, 1.5, true, $pwd, $tls);

 // 輸出串連結果。
 var_dump($obj_cluster);

 if ($obj_cluster->set("foo", "bar") == false) {
     die($obj_cluster->getLastError());
 }
 $value = $obj_cluster->get("foo");
 echo $value;
 echo "\n";
 ?>

C#

本樣本以StackExchange.Redis用戶端為例,推薦使用最新版本。

using StackExchange.Redis;
using System;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace TairClient
{
    class Program
    {
        static void Main()
        {
            // 開直連地址。
            const string Host = "r-2zee50zxi5iiqm****.redis.rds-aliyun.rds.aliyuncs.com";
            const int Port = 6379;
            Console.WriteLine("connecting...");
            var config = new ConfigurationOptions
            {
                EndPoints = { { Host, Port } },
                Ssl = true,
                Password = "Pas***23",
            };
            config.CertificateValidation += (sender, cert, chain, errors) =>
            {
                if (errors == SslPolicyErrors.RemoteCertificateChainErrors || errors ==  SslPolicyErrors.RemoteCertificateNameMismatch)
                {
                    return true;
                }
                var caCert = LoadCertificateFromPem("/root/ApsaraDB-CA-Chain.pem");

                var isCertIssuedByTrustedCA = chain.ChainElements
                    .Cast<X509ChainElement>()
                    .Any(x => x.Certificate.Thumbprint.Equals(caCert.Thumbprint, StringComparison.OrdinalIgnoreCase));

                // 自訂其它的驗證邏輯...
                return isCertIssuedByTrustedCA;
            };

            using (var conn = ConnectionMultiplexer.Connect(config))
            {
                Console.WriteLine("connected");
                var db = conn.GetDatabase();
                db.StringSet("hello", "world");
                Console.WriteLine(db.StringGet("hello")); // writes: world
            }
        }

        private static X509Certificate2 LoadCertificateFromPem(string pemFilePath)
        {
            // 使用 X509Certificate2 的靜態方法直接從 PEM 內容載入認證
            X509Certificate2 cert = X509Certificate2.CreateFromPem(File.ReadAllText(pemFilePath));
            return cert;
        }

    }
}

Spring Data Redis

本樣本以Spring Data Redis 2.7.5版(Java 1.8版本)為例,推薦使用最新版本。

With Jedis(推薦)

import java.io.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class RedisConfigJedis {
    private static SSLSocketFactory createTrustStoreSSLSocketFactory(String jksFile) throws Exception {
        KeyStore trustStore = KeyStore.getInstance("jks");
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream(jksFile);
            trustStore.load(inputStream, null);
        } finally {
            inputStream.close();
        }

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
        trustManagerFactory.init(trustStore);
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(null, trustManagers, new SecureRandom());
        return sslContext.getSocketFactory();
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory() throws Exception {
        String host = "r-2zee50zxi5iiqm****.redis.rds-aliyun.rds.aliyuncs.com:6379";
        String user = "default";
        String password = "Pas***23";
        String trustStoreFilePath = "/root/ApsaraDB-CA-Chain.jks";

        List<String> clusterNodes = Arrays.asList(host);
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(clusterNodes);
        redisClusterConfiguration.setUsername(user);
        redisClusterConfiguration.setPassword(password);

        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        // 最大空閑串連數,由於直連模式為用戶端直接連接某個資料庫分區,需要保證:業務機器數 * MaxTotal < 單個資料庫分區的最大串連數。
        jedisPoolConfig.setMaxTotal(30);
        jedisPoolConfig.setMaxIdle(20);
        jedisPoolConfig.setMinIdle(20);

        final SSLSocketFactory sslSocketFactory = createTrustStoreSSLSocketFactory(trustStoreFilePath);
        JedisClientConfiguration jedisClientConfiguration = JedisClientConfiguration.builder().useSsl()
            .sslSocketFactory(sslSocketFactory).and().usePooling().poolConfig(jedisPoolConfig).build();

        return new JedisConnectionFactory(redisClusterConfiguration, jedisClientConfiguration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

With Lettuce

import java.io.File;

import io.lettuce.core.ClientOptions;
import io.lettuce.core.SslOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig {
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        String host = "r-2zee50zxi5iiqm****.redis.rds-aliyun.rds.aliyuncs.com";
        int port = 6379;
        String user = "default";
        String password = "Pas***23";
        String trustStoreFilePath = "/root/ApsaraDB-CA-Chain.jks";

        ClientOptions clientOptions = ClientOptions.builder().sslOptions(
            SslOptions.builder().jdkSslProvider().truststore(new File(trustStoreFilePath)).build()).build();

        RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
        clusterConfiguration.clusterNode(host, port);
        clusterConfiguration.setUsername(user);
        clusterConfiguration.setPassword(password);

        LettuceClientConfiguration lettuceClientConfiguration = LettuceClientConfiguration.builder()
            .clientOptions(clientOptions)
            .useSsl()
            .disablePeerVerification()
            .build();
        return new LettuceConnectionFactory(clusterConfiguration, lettuceClientConfiguration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

Lettuce

本樣本以Lettuce 6.3.0.RELEASE版本為例,推薦使用最新版本。

import java.io.File;
import java.time.Duration;

import io.lettuce.core.RedisURI;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.SocketOptions.KeepAliveOptions;
import io.lettuce.core.SocketOptions.TcpUserTimeoutOptions;
import io.lettuce.core.SslOptions;
import io.lettuce.core.SslVerifyMode;
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;

public class SSLClusterExample {
    /**
     *  TCP_KEEPALIVE 開啟,並且配置三個參數分別為:
     *  TCP_KEEPIDLE = 30
     *  TCP_KEEPINTVL = 10
     *  TCP_KEEPCNT = 3
     */
    private static final int TCP_KEEPALIVE_IDLE = 30;

    /**
     * TCP_USER_TIMEOUT 可以避免在故障宕機情境下 Lettuce 持續逾時的問題
     * refer: https://github.com/lettuce-io/lettuce-core/issues/2082
     */
    private static final int TCP_USER_TIMEOUT = 30;

    public static void main(String[] args) throws Exception {
        String host = "r-2zee50zxi5iiqm****.redis.rds-aliyun.rds.aliyuncs.com";
        int port = 6379;
        String password = "Pas***23";
        String trustStoreFilePath = "/root/ApsaraDB-CA-Chain.jks";

        RedisURI uri = RedisURI.builder()
            .withHost(host)
            .withPort(port)
            .withPassword(password.toCharArray())
            .withSsl(true)
            .withVerifyPeer(SslVerifyMode.CA) // 因為直連叢集的特殊性,SslVerifyMode.FULL 無法使用,需要跳過 hostname 的驗證。
            .build();

        SslOptions sslOptions = SslOptions.builder()
            .jdkSslProvider()
            .truststore(new File(trustStoreFilePath)).build();

        ClusterTopologyRefreshOptions refreshOptions = ClusterTopologyRefreshOptions.builder()
            .enablePeriodicRefresh(Duration.ofSeconds(15))
            .dynamicRefreshSources(false)
            .enableAllAdaptiveRefreshTriggers()
            .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(15)).build();

        // Config TCP KeepAlive
        SocketOptions socketOptions = SocketOptions.builder()
            .keepAlive(KeepAliveOptions.builder()
                .enable()
                .idle(Duration.ofSeconds(TCP_KEEPALIVE_IDLE))
                .interval(Duration.ofSeconds(TCP_KEEPALIVE_IDLE / 3))
                .count(3)
                .build())
            .tcpUserTimeout(TcpUserTimeoutOptions.builder()
                .enable()
                .tcpUserTimeout(Duration.ofSeconds(TCP_USER_TIMEOUT))
                .build())
            .build();

        RedisClusterClient redisClient = RedisClusterClient.create(uri);
        redisClient.setOptions(ClusterClientOptions.builder()
            .socketOptions(socketOptions)
            .sslOptions(sslOptions)
            .validateClusterNodeMembership(false)
            .topologyRefreshOptions(refreshOptions).build());

        StatefulRedisClusterConnection<String, String> connection = redisClient.connect();
        connection.sync().set("key", "value");
        System.out.println(connection.sync().get("key"));
    }
}

Go

本樣本以go-redis v9.5.1用戶端為例,推薦使用v9.0以上版本。

package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"crypto/tls"
	"crypto/x509"
	"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func main() {
        caCert, err := ioutil.ReadFile("/root/ApsaraDB-CA-Chain.pem")
	if err != nil {
		fmt.Println("Error loading CA certificate:", err)
		return
	}

	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)

	tlsConfig := &tls.Config{
		RootCAs:            caCertPool,
		InsecureSkipVerify: true, // Not actually skipping, we check the cert in VerifyPeerCertificate
		VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
			// Code copy/pasted and adapted from
			// https://github.com/golang/go/blob/81555cb4f3521b53f9de4ce15f64b77cc9df61b9/src/crypto/tls/handshake_client.go#L327-L344, but adapted to skip the hostname verification.
			// See https://github.com/golang/go/issues/21971#issuecomment-412836078.

			// If this is the first handshake on a connection, process and
			// (optionally) verify the server's certificates.
			certs := make([]*x509.Certificate, len(rawCerts))
			for i, asn1Data := range rawCerts {
				cert, err := x509.ParseCertificate(asn1Data)
				if err != nil {
					panic(err)
				}
				certs[i] = cert
			}

			opts := x509.VerifyOptions{
				Roots:         caCertPool,
				DNSName:       "", // <- skip hostname verification
				Intermediates: x509.NewCertPool(),
			}

			for i, cert := range certs {
				if i == 0 {
					continue
				}
				opts.Intermediates.AddCert(cert)
			}
			_, err := certs[0].Verify(opts)
			return err
		},
	}

	rdb := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs:    []string{"r-2zee50zxi5iiqm****.redis.rds-aliyun.rds.aliyuncs.com:6379"},
		Username: "default",
		Password: "Pas***23",
		TLSConfig: tlsConfig,
	})

	err = rdb.Set(ctx, "key", "value", 0).Err()
	if err != nil {
		panic(err)
	}

	val, err := rdb.Get(ctx, "key").Result()
	if err != nil {
		panic(err)
	}
	fmt.Println("key:", val)
}

常見問題

  • 為什麼報錯No subject alternative DNS name matching xxx found

    在開啟TLS後,若您修改了執行個體的串連地址或連接埠號碼,再通過TLS串連執行個體時會出現上述報錯。請您在控制台中更新TLS認證後重試。