You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中使用Jedis连接Redis TLS时,多证书密钥库如何选正确SSL别名?

问题:多证书密钥库下Jedis通过TLS连接Redis的证书选择问题

我尝试通过TLS连接Redis,当密钥库仅含单个证书时连接正常。但当密钥库导入多个证书时,Jedis如何选择正确别名以获取对应密钥?

我自行实现X509KeyManager探究机制,发现chooseClientAlias(String[] strings, Principal[] prncpls, Socket socket)方法传入的prncpls为空数组——我原以为可通过该参数判断应使用的证书。

由于prncpls为空,方法仅返回与strings中指定密钥类型(如RSA)匹配的第一个别名,该别名可能不正确,进而导致选择错误密钥,SSL连接失败。

我是否误解了该机制?比如是否需要为每个对接的SSL应用创建不同的SSLSocketFactory和KeyManager,并显式指定别名?


解答

为什么prncpls参数为空?

Redis在TLS握手的默认配置下,不会发送可接受的客户端证书颁发者列表,因此Java的X509KeyManager中chooseClientAlias方法的prncpls(即issuers)参数会是空数组。这是Redis的默认行为,并非代码逻辑问题。即使开启Redis的tls-auth-clients yes要求客户端提供证书,Redis通常也不会发送具体的颁发者列表,所以该参数依然为空。

解决方案

方案1:自定义X509KeyManager强制指定固定别名

通过包装默认的X509KeyManager,直接返回你需要使用的证书别名,彻底避免自动选择错误的问题。修改后的createSslSocketFactory方法示例:

private static SSLSocketFactory createSslSocketFactory(
            String caCertPath, String caCertPassword, String userCertPath, String userCertPassword, String targetAlias)
            throws IOException, GeneralSecurityException {

        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(new FileInputStream(userCertPath), userCertPassword.toCharArray());

        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(new FileInputStream(caCertPath), caCertPassword.toCharArray());

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        trustManagerFactory.init(trustStore);

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        keyManagerFactory.init(keyStore, userCertPassword.toCharArray());

        // 包装默认KeyManager,强制返回指定别名
        KeyManager[] keyManagers = Arrays.stream(keyManagerFactory.getKeyManagers())
                .map(km -> {
                    if (km instanceof X509KeyManager) {
                        return new X509KeyManager() {
                            private final X509KeyManager delegate = (X509KeyManager) km;

                            @Override
                            public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
                                // 直接返回目标证书别名
                                return targetAlias;
                            }

                            // 其他方法委托给原KeyManager实现
                            @Override
                            public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
                                return delegate.chooseServerAlias(keyType, issuers, socket);
                            }

                            @Override
                            public String[] getClientAliases(String keyType, Principal[] issuers) {
                                return delegate.getClientAliases(keyType, issuers);
                            }

                            @Override
                            public String[] getServerAliases(String keyType, Principal[] issuers) {
                                return delegate.getServerAliases(keyType, issuers);
                            }

                            @Override
                            public X509Certificate[] getCertificateChain(String alias) {
                                return delegate.getCertificateChain(alias);
                            }

                            @Override
                            public PrivateKey getPrivateKey(String alias) {
                                return delegate.getPrivateKey(alias);
                            }
                        };
                    }
                    return km;
                })
                .toArray(KeyManager[]::new);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagers, trustManagerFactory.getTrustManagers(), null);

        return sslContext.getSocketFactory();
}

调用时传入目标证书别名:

SSLSocketFactory sslFactory = createSslSocketFactory(
        "D:\\tmp\\keystore.jks",
        "123456",
        "D:\\tmp\\keystore.jks",
        "123456",
        "my_cert" // 指定要使用的证书别名
);

方案2:为不同服务创建独立密钥库

如果公司环境允许,可为每个需要对接的SSL服务(如Redis、其他内部API)单独创建密钥库,仅放入对应服务所需的证书和密钥。这样默认的KeyManager只会找到唯一的证书,无需担心选择错误。


相关测试信息

证书生成命令

# 创建CA证书
"C:\Program Files\Git\mingw64\bin\openssl.exe" genrsa -out ca.key 2048
"C:\Program Files\Git\mingw64\bin\openssl.exe" req -new -x509 -sha256 -key ca.key -out ca.crt

# 创建Redis服务端证书
"C:\Program Files\Git\mingw64\bin\openssl.exe" genrsa -out redis.key
"C:\Program Files\Git\mingw64\bin\openssl.exe" req -new -sha256 -key redis.key -out redis.csr
"C:\Program Files\Git\mingw64\bin\openssl.exe" x509 -req -in redis.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out redis.crt -days 1000 -sha256

# 创建客户端证书
"C:\Program Files\Git\mingw64\bin\openssl.exe" genrsa -out client1.key 2048
"C:\Program Files\Git\mingw64\bin\openssl.exe" req -new -sha256 -key client1.key -out client1.csr
"C:\Program Files\Git\mingw64\bin\openssl.exe" x509 -req -in client1.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client1.crt -days 1000 -sha256

证书导入密钥库命令

# 将CA证书导入信任库
keytool -import -alias redisCA -keystore keystore.jks -file ca.crt

# 生成PKCS12格式的客户端证书/密钥
openssl pkcs12 -export -in client1.crt -inkey client1.key -out keystore.p12 -name my_cert

# 将PKCS12导入JKS密钥库
keytool -importkeystore -destkeystore keystore.jks -srckeystore keystore.p12 -srcstoretype PKCS12 -alias my_cert

原对接Redis代码

public void testWithTls() throws IOException, GeneralSecurityException {
        HostAndPort address = new HostAndPort("localhost", 6379);
        
        SSLSocketFactory sslFactory = createSslSocketFactory(
                "D:\\tmp\\keystore.jks",
                "123456",
                "D:\\tmp\\keystore.jks",
                "123456"
        );

        JedisClientConfig config = DefaultJedisClientConfig.builder()
                .ssl(true).sslSocketFactory(sslFactory)
                .build();

        JedisPooled jedis = new JedisPooled(address, config);
        jedis.set("foo", "bar");
        System.out.println(jedis.get("foo")); // prints bar
}
   
private static SSLSocketFactory createSslSocketFactory(
            String caCertPath, String caCertPassword, String userCertPath, String userCertPassword)
            throws IOException, GeneralSecurityException {

        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(new FileInputStream(userCertPath), userCertPassword.toCharArray());

        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(new FileInputStream(caCertPath), caCertPassword.toCharArray());

        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        trustManagerFactory.init(trustStore);

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        keyManagerFactory.init(keyStore, userCertPassword.toCharArray());

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

        return sslContext.getSocketFactory();
}

环境信息

  • Jedis版本:4.4.3
  • Redis Docker容器版本:redis:7.0.10
  • Redis启动命令:redis-server --tls-port 6379 --port 0 --tls-cert-file /tls/redis.crt --tls-key-file /tls/redis.key --tls-ca-cert-file /tls/ca.crt --loglevel warning
  • 使用JKS而非PKCS12的原因:公司环境要求

内容的提问来源于stack exchange,提问作者NPC

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 22:05:33