Java中使用Jedis连接Redis TLS时,多证书密钥库如何选正确SSL别名?
我尝试通过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

