[입개발] Spring-data-redis 에서 Jedis로 TLS를 쓰면서 인증서 체크는 Disable 하는 방법

흐음… 최근에 Redis를 TLS로 사용해야 할 일이 생겼습니다.(아시는 분은 아시겠지만, 저는 자바맹, Spring 맹이라…) Spring-data-redis 를 쓰는 서비스에서 TLS를 키는 것은 사실 아주아주아주 쉽습니다. 일단 저희는 spring-data-redis에서 Jedis를 쓰고 있는 상황입니다.(여기서 왜 lettuce 안쓰고 Jedis 쓰냐고 물으시면, 원래 그렇게 만들어져 있었으니까라고 대답을 ㅎㅎㅎ)

그런데 JedisConenctionFactory에서 TLS를 쓰는 건 아주 쉽습니다. 그냥 setUseSsl(true)만 호출하면 그 때부터 TLS가 딱!!! 됩니다.

딱!!! 되는데 제대로 접속하기 위해서는 인증서가 제대로 된 위치에 존재해야 합니다. 그런데, 내부 레디스 서버마다 인증서를 만들기는 귀찮을 수 있습니다. 그럴때는 살포시… 인증서를 무시해주면 되는데…

구글링을 해봐도, Spring Data Redis에 TLS를 적용하는 방법은 많이 나옵니다. 또, 인증서 체크를 Disable 하는 방법도 꽤 많이 나옵니다. 그런데 인증서 체크를 끄는 법은 다 HTTPS 관련 Rest Template 설정에 관한 것들입니다. 즉 아주 편하게 할 수 있는 Spring Data Redis 에서 TLS를 인증서 체크를 끄는 방법이 검색해도 안나오더라는…(아마도 제가 못 찾은걸 확신합니다만…)

일단 기본적인 방법은 다음과 같습니다.(다음 StackOverflow 를 참고합니다.)

  1. TrustManager를 생성한다.
  2. SSLContext를 가져온다.
  3. SSLContext의 init를 아까 생성한 TrushManager를 이용하도록 설정한다.
  4. 해당 SSLContext의 SSLSocketFactory를 이용하도록 설정한다.
import javax.net.ssl.*;
import java.security.*;
import java.security.cert.X509Certificate;

public final class SSLUtil{

    private static final TrustManager[] UNQUESTIONING_TRUST_MANAGER = new TrustManager[]{
            new X509TrustManager() {
                public java.security.cert.X509Certificate[] getAcceptedIssuers(){
                    return null;
                }
                public void checkClientTrusted( X509Certificate[] certs, String authType ){}
                public void checkServerTrusted( X509Certificate[] certs, String authType ){}
            }
        };

    public  static void turnOffSslChecking() throws NoSuchAlgorithmException, KeyManagementException {
        // Install the all-trusting trust manager
        final SSLContext sc = SSLContext.getInstance("SSL");
        sc.init( null, UNQUESTIONING_TRUST_MANAGER, null );
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
    }

    public static void turnOnSslChecking() throws KeyManagementException, NoSuchAlgorithmException {
        // Return it to the initial state (discovered by reflection, now hardcoded)
        SSLContext.getInstance("SSL").init( null, null, null );
    }

    private SSLUtil(){
        throw new UnsupportedOperationException( "Do not instantiate libraries.");
    }
}

위의 코드는 HttpsURLConnection 을 위한 SSL 인증서 체크를 무시하는 방법입니다. 다만, 우리는 이걸 보면, 아 Spring Data Redis의 JedisConnectionFactory 도 유사하다고 추측할 수 있습니다. 실제로 JedisConnectionFactory 코드를 보면 SSLSocketFactory를 가져오는 함수는 보이지 않습니다.

조금 더 파보면 JedisConnectionFactory 안에는 clientConfiguration 변수가 있고, 다음과 같은 setSslSocketFactory, getSslSocketFactory 와 같은 함수들이 보입니다.

static class MutableJedisClientConfiguration implements JedisClientConfiguration {
......
		@Override
		public Optional getSslSocketFactory() {
			return Optional.ofNullable(sslSocketFactory);
		}

		public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
			this.sslSocketFactory = sslSocketFactory;
		}

		/*
		 * (non-Javadoc)
		 * @see org.springframework.data.redis.connection.jedis.JedisClientConfiguration#getSslParameters()
		 */
		@Override
		public Optional getSslParameters() {
			return Optional.ofNullable(sslParameters);
		}

		public void setSslParameters(SSLParameters sslParameters) {
			this.sslParameters = sslParameters;
		}
......
}

그리고 createJedis 같은 함수를 보면 아래와 같이 Jedis 인스턴스를 생성할 때 clientConfiguration의 getSslSocketFactory 함수를 쓰고 있는걸 볼 수 있습니다.

	private Jedis createJedis() {

		if (providedShardInfo) {
			return new Jedis(getShardInfo());
		}

		Jedis jedis = new Jedis(getHostName(), getPort(), getConnectTimeout(), getReadTimeout(), isUseSsl(),
				clientConfiguration.getSslSocketFactory().orElse(null), //
				clientConfiguration.getSslParameters().orElse(null), //
				clientConfiguration.getHostnameVerifier().orElse(null));

		Client client = jedis.getClient();

		getRedisPassword().map(String::new).ifPresent(client::setPassword);
		client.setDb(getDatabase());

		return jedis;
	}

오호 이제 저 setSslSocketFactory 함수를 통해서 아까 얻은 SSLContext의 SSLSocketFactory 로 바꿔주면 될듯 합니다. 그런데 아주 사소한 문제가 있습니다. 어떻게 JedisConnectionFactory에서 저 값을 바꿀 수 있을까요? 살짝 살펴보니 오오 다음과 같은 함수가 존재합니다.

public class JedisConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory {
        ......
        public JedisClientConfiguration getClientConfiguration() {
		return clientConfiguration;
	}
        ......
}

그런데, 오예!! 하면서 받아서 setSslSocketFactory를 호출해보려고 하면…. 문제가 발생합니다. 그것은!!!, JedisClientConfiguration 이 interface 인데… ReadOnly Interface라는 것입니다. 대략 다음과 같습니다.

public interface JedisClientConfiguration {
	boolean isUseSsl();
	Optional getSslSocketFactory();
	Optional getSslParameters();
	Optional getHostnameVerifier();
	boolean isUsePooling();
	Optional getPoolConfig();
	Optional getClientName();
	Duration getConnectTimeout();
	Duration getReadTimeout();
        ......
}

흐음, 그러면 어차피 내부는 MutableJedisClientConfiguration 클래스이니, 그냥 강제 형변환해버리면 되지 않을까요? 일단 제가 자바에 깊은 지식이 없어서… 실패했을 수도 있지만, Inner Class 고, 같은 패키지가 아니면 사용할 수가 없습니다. 그럼 무슨 말이야… 앞에 시도한 방법은 모두 실패… 우리는 이상한 산으로 가고 있던 것입니다.

그럼 어떻게 해야 하며, 해당 소스를 살펴보니, 다행히 다음과 같은 생성자가 있습니다. 잘 살펴보면 RedisStandaloneConfiguration 과 JedisClientConfiguration 을 생성자로 받고 있습니다.

public JedisConnectionFactory(RedisStandaloneConfiguration standaloneConfig, JedisClientConfiguration clientConfig) {

		this(clientConfig);

		Assert.notNull(standaloneConfig, "RedisStandaloneConfiguration must not be null!");

		this.standaloneConfig = standaloneConfig;
	}

이제 대충 방법이 떠오르시나요? 즉, 다음과 같이 JedisClientConfiguration 을 상속받는 다른 클래스를 만들어서 JedisConnectionFactory 의 생성자로 넘기면, 우리가 원하는 동작을 하도록 만들 수가 있습니다.

대략 저는 다음과 같은 JedisSSLClientConfiguration 이라는 클라스를 만들었습니다. 그리고 JedisConnectionFactory 에 넘겨주니, 인증서 체크를 잘 회피하면서 동작하게 되었습니다. 역시 소스를 보면 길이 보이는 경우가 종종 있네요. 자바를 잘하시는 분들이 부럽습니다. 전 맨날 삽질만…

    JedisSSLClientConfiguration() {
        setUsePooling(true);
        setUseSsl(true);

        TrustManager[] trustAllCerts = new TrustManager[] {
                new X509TrustManager() {
                    public java.security.cert.X509Certificate[] getAcceptedIssuers()
                    {
                        return new X509Certificate[0];
                    }
                    public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
                    {
                    }
                    public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
                    {}
                }
        };

        try {
            SSLContext sc = SSLContext.getInstance("TLS");
            sc.init(null, trustAllCerts, null);
            sslSocketFactory = sc.getSocketFactory();
            sslParameters = new SSLParameters();
            sslParameters.setEndpointIdentificationAlgorithm("");
        } catch (Exception e) {
            throw new RuntimeException(e.toString());
        }

        hostnameVerifier = new HostnameVerifier() {
            @Override
            public boolean verify(String s, SSLSession sslSession) {
                return true;
            }
        };
    }