본문 바로가기
  • _^**_
무근본 IT 지식 공유/무근본 스프링(Spring Framework)

[무근본 스프링] 클라이언트 상호 인증서 방식 사용하기(발급부터 사용까지)

by 크리드로얄워터 2023. 4. 15.
반응형

웹에서 클라이언트 상호 인증서 방식을 사용하는 이유

웹에서 클라이언트 상호 인증서 방식을 사용하는 이유는 다음과 같습니다.

보안 강화
일반적인 HTTP 프로토콜을 사용하는 웹 서비스는 암호화되지 않은 데이터를 전송하기 때문에, 중간에 데이터를 가로채어서 정보를 탈취할 수 있는 보안 취약점이 존재합니다. HTTPS 프로토콜은 데이터를 암호화하여 전송하기 때문에, 이러한 보안 취약점을 보완할 수 있습니다. 클라이언트 상호 인증서 방식을 사용하면, 서버와 클라이언트 간의 상호 인증을 수행하여 더욱 보안을 강화할 수 있습니다.

불법 접근 방지
일반적인 로그인 방식은 사용자 ID와 비밀번호를 입력하여 인증하는 방식입니다. 이 경우, 타인이 사용자 ID와 비밀번호를 알아내어 불법적으로 접근할 수 있는 취약점이 존재합니다. 클라이언트 상호 인증서 방식을 사용하면, 클라이언트가 발급받은 인증서를 사용하여 서버와 인증을 수행하기 때문에, 이러한 취약점을 보완할 수 있습니다.

보안성과 편의성의 균형
클라이언트 상호 인증서 방식은 기존의 로그인 방식과 비교하여 더욱 보안성이 높은 방식입니다. 그러나, 클라이언트가 인증서를 발급받고 로컬에 저장해야 하기 때문에, 일반적인 로그인 방식보다는 조금 더 번거로울 수 있습니다. 따라서, 보안성과 편의성의 균형을 유지하는 것이 중요합니다.

따라서, 클라이언트 상호 인증서 방식을 사용하여 보안성을 강화하면서도 사용자의 편의성을 유지할 수 있습니다.

 

클라이언트 인증서 방식으로 서버에서 인증서를 발급받고 로컬에 인증서를 저장하고, 인증서를 사용하여 HTTPS 통신을 수행하는 전체 과정

클라이언트는 자바스크립트, 서버는 스프링 프레임워크라고 가정한다

1. 클라이언트(Javascript)에서 인증서 방식으로 스프링 프레임워크 서버와 통신하는 예제

// 인증서 발급받기
var xhr = new XMLHttpRequest();
xhr.open("GET", "/certificate", true);
xhr.responseType = "arraybuffer";
xhr.onload = function() {
    if (xhr.readyState === xhr.DONE) {
        if (xhr.status === 200) {
            // 인증서 저장하기
            var cert = new Uint8Array(xhr.response);
            var blob = new Blob([cert], {type: "application/x-x509-ca-cert"});
            var filename = "client.crt";
            if (navigator.msSaveBlob) {
                navigator.msSaveBlob(blob, filename);
            } else {
                var a = document.createElement("a");
                if (a.download !== undefined) {
                    var url = URL.createObjectURL(blob);
                    a.href = url;
                    a.download = filename;
                    a.style.display = "none";
                    document.body.appendChild(a);
                    a.click();
                    window.URL.revokeObjectURL(url);
                    document.body.removeChild(a);
                }
            }
            // HTTPS 통신하기
            var keystorePassword = "password";
            var url = "https://localhost:8443";
            var pkcs12 = new Uint8Array();
            var xhr = new XMLHttpRequest();
            xhr.open("GET", "/client.p12", true);
            xhr.responseType = "arraybuffer";
            xhr.onload = function() {
                if (xhr.readyState === xhr.DONE) {
                    if (xhr.status === 200) {
                        pkcs12 = new Uint8Array(xhr.response);
                        var p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(pkcs12), false, keystorePassword);
                        var bags = p12.getBags({bagType: forge.pki.oids.certBag});
                        var cert = bags[forge.pki.oids.certBag][0].cert;
                        var key = p12.getKey(cert);
                        var caStore = forge.pki.createCaStore();
                        caStore.addCertificate(cert);
                        var sslContext = forge.tls.createConnection({
                            server: false,
                            caStore: caStore,
                            sessionCache: {},
                            verify: function(connection, verified, depth, certs) {
                                return true;
                            },
                            getCertificate: function(connection, hint) {
                                return cert;
                            },
                            getPrivateKey: function(connection, cert) {
                                return key;
                            }
                        });
                        xhr = new XMLHttpRequest();
                        xhr.open("GET", url, true);
                        xhr.onreadystatechange = function() {
                            if (xhr.readyState === xhr.DONE) {
                                if (xhr.status === 200) {
                                    console.log(xhr.responseText);
                                }
                            }
                        };
                        xhr.send();
                    }
                }
            };
            xhr.send();
        }
    }
};
xhr.send();

위 예제에서는 XMLHttpRequest 객체를 사용하여 GET 요청을 수행합니다. 이때, /certificate URL에 요청을 보내어 서버에서 클라이언트 인증서를 발급받습니다. 응답으로 받은 인증서를 Blob 객체로 생성하여 로컬에 저장합니다.

인증서를 로컬에 저장한 후, XMLHttpRequest 객체를 다시 생성합니다. 이때, /client.p12 URL에 요청을 보내어 클라이언트 인증서 파일을 로딩합니다. 응답으로 받은 클라이언트 인증서 파일을 사용하여 인증서와 개인 키를 로딩합니다. 그리고 forge.tls.createConnection() 함수를 사용하여 sslContext 객체를 생성합니다. 이때, caStore 객체를 사용하여 클라이언트 인증서를 등록합니다.

XMLHttpRequest 객체를 다시 생성하고, open() 메서드를 사용하여 GET 요청을 수행합니다. 이때, xhr.onreadystatechange() 콜백 함수를 등록하여 요청 결과를 처리합니다. 요청이 성공하면, 서버에서 전송한 응답 데이터가 xhr.responseText에 저장됩니다.

서버 측에서는 스프링 프레임워크를 사용하여 클라이언트 인증서를 발급합니다. 

 

2. 스프링 프레임워크에서 클라이언트 인증서를 발급하는 예제

@Configuration
public class SslConfig {

    @Bean
    public KeyStore keyStore() throws Exception {
        char[] password = "password".toCharArray();
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        keyStore.load(new FileInputStream("client.p12"), password);
        return keyStore;
    }

    @Bean
    public X509Certificate clientCertificate() throws Exception {
        Certificate cert = keyStore().getCertificate("myalias");
        return (X509Certificate) cert;
    }

    @Bean
    public KeyManagerFactory keyManagerFactory() throws Exception {
        char[] password = "password".toCharArray();
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore(), password);
        return keyManagerFactory;
    }

    @Bean
    public TrustManagerFactory trustManagerFactory() throws Exception {
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(null, null);
        trustStore.setCertificateEntry("client", clientCertificate());
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(trustStore);
        return trustManagerFactory;
    }

    @Bean
    public SSLContext sslContext() throws Exception {
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagerFactory().getKeyManagers(), trustManagerFactory().getTrustManagers(), null);
        return sslContext;
    }

    @Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory() {
            @Override
            protected void postProcessContext(Context context) {
                SecurityConstraint securityConstraint = new SecurityConstraint();
                securityConstraint.setUserConstraint("CONFIDENTIAL");
                
				SecurityCollection collection = new SecurityCollection();
				collection.addPattern("/*");
				securityConstraint.addCollection(collection);
				context.addConstraint(securityConstraint);
			}

		};

		factory.addAdditionalTomcatConnectors(createHttpConnector());
		factory.setSsl(ssl());
		return factory;
	}

	private Connector createHttpConnector() {
    	Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
    	connector.setScheme("http");
    	connector.setPort(8080);
    	connector.setSecure(false);
    	connector.setRedirectPort(8443);
    	return connector;
	}

	private Ssl ssl() {
    	Ssl ssl = new Ssl();
    	ssl.setKeyStoreType("PKCS12");
    	ssl.setKeyStore("classpath:client.p12");
    	ssl.setKeyStorePassword("password");
    	ssl.setKeyPassword("password");
    	return ssl;
	}
    
}

위 예제에서는 `SslConfig` 클래스를 정의하여 클라이언트 인증서를 발급합니다. `keyStore()` 메서드를 사용하여 클라이언트 인증서 파일을 로딩합니다. `clientCertificate()` 메서드를 사용하여 인증서를 추출합니다. `keyManagerFactory()` 메서드와 `trustManagerFactory()` 메서드를 사용하여 키 매니저와 트러스트 매니저를 생성합니다. `sslContext()` 메서드를 사용하여 SSL 컨텍스트를 생성합니다.

`servletContainer()` 메서드를 사용하여 톰캣 서버를 생성합니다. `createHttpConnector()` 메서드를 사용하여 HTTP 커넥터를 생성합니다. `ssl()` 메서드를 사용하여 SSL 설정을 구성합니다.

이제 클라이언트에서 `/certificate` URL로 요청을 보내면, 서버에서 클라이언트 인증서를 발급할 수 있습니다. 클라이언트는 서버에서 발급한 클라이언트 인증서를 로컬에 저장한 후, HTTPS 요청을 수행할 때 인증서를 사용하여 인증을 수행합니다.

반응형

댓글