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.p12 및 servertruststore.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.keyStore 및 javax.net.ssl을 설정합니다. keytool 로 이전에 생성한 clientkeystore.p12 및 clienttruststore.jks 파일 을 가리키는 trustStore 시스템 속성 입니다.
7. 결론
양방향 TLS 인증을 수행하기 위해 서버 및 클라이언트 인증서를 사용하는 간단한 클라이언트-서버 Java 구현을 작성했습니다 .
keytool 을 사용 하여 자체 서명된 인증서를 생성했습니다.
예제의 소스 코드는 GitHub 에서 찾을 수 있습니다 .