1. 개요

HTTPS는 컴퓨터 네트워크의 두 엔터티 간에 Security 통신을 허용하는 HTTP의 확장입니다. HTTPS는 Security 연결을 달성하기 위해 TLS (Transport Layer Security) 프로토콜을 사용합니다.

TLS는 단방향 또는 양방향 인증서 확인으로 구현할 수 있습니다 . 단방향에서는 서버가 공용 인증서를 공유하므로 클라이언트가 신뢰할 수 있는 서버인지 확인할 수 있습니다. 대안은 양방향 확인입니다. 클라이언트와 서버는 서로의 신원을 확인하기 위해 공용 인증서를 공유합니다 .

이 문서에서는 서버가 클라이언트의 인증서도 확인하는 양방향 인증서 확인에 중점을 둘 것입니다 .

2. 자바 및 TLS 버전

TLS 1.3은 프로토콜의 최신 버전입니다. 이 버전은 더 성능이 좋고 안전 합니다. 보다 효율적인 핸드셰이크 프로토콜이 있으며 최신 암호화 알고리즘을 사용합니다.

Java는 Java 11에서 이 버전의 프로토콜을 지원하기 시작했습니다. 우리는 이 버전을 사용하여 인증서를 생성하고 TLS를 사용하여 서로를 인증하는 간단한 클라이언트-서버 쌍을 구현할 것입니다.

3. Java에서 인증서 생성

양방향 TLS 인증 을 수행하고 있으므로 클라이언트와 서버에 대한 인증서를 생성해야 합니다.

프로덕션 환경에서는 인증 기관에서 인증서를 구입하는 것이 좋습니다. 그러나 테스트 또는 데모용으로 자체 서명된 인증서 를 사용하는 것으로 충분합니다 . 이 기사에서는 Java의 keytool 을 사용하여 자체 서명된 인증서를 생성할 것입니다.

3.1. 서버 인증서

먼저  서버 키 저장소를 생성합니다 .

keytool -genkey -alias serverkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore serverkeystore.p12 -storepass password -ext san=ip:127.0.0.1,dns:localhost

keytool -ext 옵션을 사용하여 SAN(주체 대체 이름)을 설정하여 서버를 식별하는 로컬 호스트 이름/IP 주소를 정의합니다. 일반적으로 이 옵션으로 여러 주소를 지정할 수 있습니다. 그러나 클라이언트는 이러한 주소 중 하나를 사용하여 서버에 연결하도록 제한됩니다.

다음으로 인증서를 server-certificate.pem 파일로 내보냅니다 .

keytool -exportcert -keystore serverkeystore.p12 -alias serverkey -storepass password -rfc -file server-certificate.pem

마지막으로 클라이언트의 신뢰 저장소에 서버 인증서를 추가합니다 .

keytool -import -trustcacerts -file server-certificate.pem -keypass password -storepass password -keystore clienttruststore.jks

3.2. 클라이언트 인증서

마찬가지로 클라이언트 키 저장소를 생성하고 해당 인증서를 내보냅니다.

keytool -genkey -alias clientkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore clientkeystore.p12 -storepass password -ext san=ip:127.0.0.1,dns:localhost

keytool -exportcert -keystore clientkeystore.p12 -alias clientkey -storepass password -rfc -file client-certificate.pem

keytool -import -trustcacerts -file client-certificate.pem -keypass password -storepass password -keystore servertruststore.jks

마지막 명령  에서 클라이언트의 인증서를 서버 신뢰 저장소에 추가했습니다 .

4. 서버 자바 구현

자바 소켓을 사용하면 서버 구현이 간단합니다. SSLSocketEchoServer 클래스는 TLS 인증을 쉽게 지원하기 위해 SSLServerSocket 가져옵니다 . 암호와 프로토콜만 지정하면 되고 나머지는 클라이언트가 보낸 동일한 메시지에 응답하는 표준 에코 서버입니다.

public class SSLSocketEchoServer {

    static void startServer(int port) throws IOException {

        ServerSocketFactory factory = SSLServerSocketFactory.getDefault();
        try (SSLServerSocket listener = (SSLServerSocket) factory.createServerSocket(port)) {
            listener.setNeedClientAuth(true);
            listener.setEnabledCipherSuites(new String[] { "TLS_AES_128_GCM_SHA256" });
            listener.setEnabledProtocols(new String[] { "TLSv1.3" });
            System.out.println("listening for messages...");
            try (Socket socket = listener.accept()) {
                
                InputStream is = new BufferedInputStream(socket.getInputStream());
                byte[] data = new byte[2048];
                int len = is.read(data);
                
                String message = new String(data, 0, len);
                OutputStream os = new BufferedOutputStream(socket.getOutputStream());
                System.out.printf("server received %d bytes: %s%n", len, message);
                String response = message + " processed by server";
                os.write(response.getBytes(), 0, response.getBytes().length);
                os.flush();
            }
        }
    }
}

서버는 클라이언트 연결을 수신 대기합니다. listener.setNeedClientAuth(true) 를 호출 하려면 클라이언트가 인증서를 서버와 공유해야 합니다 . 백그라운드에서 SSLServerSocket 구현은 TLS 프로토콜을 사용하여 클라이언트를 인증합니다.

우리의 경우 자체 서명된 클라이언트 인증서는 소켓이 연결을 수락하도록 서버 신뢰 저장소에 있습니다 . 서버는 InputStream 을 사용하여 메시지 읽기를 진행합니다 . 그런 다음 OuputStream 을 사용 하여 승인을 추가하는 수신 메시지를 반향합니다.

5. 클라이언트 자바 구현

서버에서 했던 것과 같은 방식으로 클라이언트 구현은 간단한 SSLScocketClient 클래스입니다.

public class SSLScocketClient {

    static void startClient(String host, int port) throws IOException {

        SocketFactory factory = SSLSocketFactory.getDefault();
        try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
            
            socket.setEnabledCipherSuites(new String[] { "TLS_AES_128_GCM_SHA256" });
            socket.setEnabledProtocols(new String[] { "TLSv1.3" });
            
            String message = "Hello World Message";
            System.out.println("sending message: " + message);
            OutputStream os = new BufferedOutputStream(socket.getOutputStream());
            os.write(message.getBytes());
            os.flush();
            
            InputStream is = new BufferedInputStream(socket.getInputStream());
            byte[] data = new byte[2048];
            int len = is.read(data);
            System.out.printf("client received %d bytes: %s%n", len, new String(data, 0, len));
        }
    }
}

먼저 서버와의 연결을 설정 하는 SSLSocket 을 만듭니다. 백그라운드에서 소켓은 TLS 연결 설정 핸드셰이크를 설정합니다. 이 핸드셰이크의 일부로 클라이언트는 서버의 인증서를 확인하고 클라이언트 신뢰 저장소 에 있는지 확인합니다 .

연결이 성공적으로 설정되면 클라이언트는 출력 스트림을 사용하여 서버에 메시지를 보냅니다. 그런 다음 입력 스트림으로 서버의 응답을 읽습니다.

6. 애플리케이션 실행

서버를 실행하려면 명령 창을 열고 다음을 실행합니다.

java -Djavax.net.ssl.keyStore=/path/to/serverkeystore.p12 \ 
  -Djavax.net.ssl.keyStorePassword=password \
  -Djavax.net.ssl.trustStore=/path/to/servertruststore.jks \ 
  -Djavax.net.ssl.trustStorePassword=password \
  com.baeldung.httpsclientauthentication.SSLSocketEchoServer

javax.net.ssl 에 대한 시스템 속성을 지정합니다 . 키 저장소javax.net.ssl. trustStore 는 이전에 keytool 로 생성한 serverkeystore.p12servertruststore.jks 파일 을 가리킵니다 .

클라이언트를 실행하려면 다른 명령 창을 열고 다음을 실행합니다.

java -Djavax.net.ssl.keyStore=/path/to/clientkeystore.p12 \ 
  -Djavax.net.ssl.keyStorePassword=password \ 
  -Djavax.net.ssl.trustStore=/path/to/clienttruststore.jks \ 
  -Djavax.net.ssl.trustStorePassword=password \ 
  com.baeldung.httpsclientauthentication.SSLScocketClient	

마찬가지로 javax.net.ssl.keyStorejavax.net.ssl을 설정합니다. keytool 로 이전에 생성한 clientkeystore.p12clienttruststore.jks 파일 을 가리키는 trustStore 시스템 속성 입니다.

7. 결론

양방향 TLS 인증을 수행하기 위해 서버 및 클라이언트 인증서를 사용하는 간단한 클라이언트-서버 Java 구현을 작성했습니다 .

keytool 을 사용 하여 자체 서명된 인증서를 생성했습니다.

예제의 소스 코드는 GitHub 에서 찾을 수 있습니다 .

Security footer banner