본문으로 바로가기

[Java] HttpsURLConnection

category 3. 웹개발/3_1_1 JAVA 2020. 8. 20. 18:32
반응형

[Java] HttpsURLConnection

 

안녕하세요. 갓대희 입니다. 이번 포스팅은 [ [Java] HttpsURLConnection ] 입니다. : ) 

 

0.HttpsURLConnection

▶ 0. HttpsURLConnection란?

 - JAVA 소스 내에서 SSL 적용된 사이트에 접근하기 위해, REST Api를 호출하기 위해 사용하게 되며, 결과 데이터를 스트림 형식으로 제공받아 이용이 가능하다. 

 - 데이터의 타입이나 길이는 거의 제한이 없으며, 주로 미리 길이를 알지 못하는 스트리밍 데이터를 주고 받는데 사용된다.

 - 더 자세한 내용은 현 시점에는 java8을 가장 많이 쓸 것으로 생각하고, java8 기준으로 HttpsURLConnection doc을 확인 해보자.

 - docs.oracle.com/javase/8/docs/api/javax/net/ssl/HttpsURLConnection.html

 - 기본 골격은 https://goddaehee.tistory.com/161 과 비슷하니 참고.(이번 포스팅에선 바로 사용할 만한 간단한 예제를 기술하려 한다.)

 

※ 무시해도 좋으나 참고하려면 참고 하자.

 - 조금더 알아보자면, 위 javadoc을 확인해보면 다음과 같이 설명하고 있다.

java.lang.Object
 └ java.net.URLConnection
   └ java.net.HttpURLConnection
     └ javax.net.ssl.HttpsURLConnection

public abstract class HttpsURLConnection extends HttpURLConnection

Fields inherited from class java.net.URLConnection
allowUserInteraction, connected, doInput, doOutput, ifModifiedSince, url, useCaches

 - HttpURLConnection을 상속 받고 있음을 볼 수 있다. (URLConnection은 이미 이전 포스팅에서 설명하였듯이 주로 URL 내용을 읽어오거나, URL 주소에 GET / POST로 데이터를 전달 할 때 사용한다.)

 - URLConnection에서 상속 받은 필드는 다음과 같다고 나와있다. 당연히 관련 필드 및 관련 메서드들도 참고로 짚고 넘어가면 좋을 것 같다.

(allowUserInteraction, connected, doInput, doOutput, ifModifiedSince, url, useCaches)

(docs.oracle.com/javase/8/docs/api/java/net/URLConnection.html)

 - URLConnection은 리소스에 연결하기 전에 구성 되어야 한다.
 - URLConnection 인스턴스는 재사용 될 수 없다. 각 리소스에 대한 커넥션 마다 다른 인스턴스를 사용해야 한다.

 

 

URLConnection 관련 필드 및 메서드

● getAllowUserInteraction() : 연결 된 곳에 사용자가 서버와 통신 할 수 있는 환경 여부를 확인한다.(boolean), in/output이 해당 서버, 연결 포트로 가능한지 확인한다. 
● getDefaultAllowUserInteraction(): 기본적으로 User와 통신 가능한 상태인지 확인한다.(boolean) 
● connect() : 해당 url에 연결 된 곳에 접속 할때 사용한다. false인 경우, 연결 객체는 지정된 URL로의 통신 링크를 작성하고 true인 경우엔 통신 링크가 설정되이 있다. 
● setDoInput() :  
 - URLConnection에 대한 doInput 필드값을 지정된 값으로 설정한다. URL 연결은 입출력에 사용될 수 있다. true로 설정시 서버통신에서 입력 가능한 상태로 설정 한다.(응답 헤더와 메시지 등을 Read) (default : true) 
● setDoOutput() :  
 - URLConnection에 대한 doOutput 필드값을 지정된 값으로 설정한다. true로 설정시 서버통신에서 출력 가능한 상태로 설정 한다.(outputstream으로 데이터 처리 등) (default : false) 
● getDoInput() : Server에서 온 데이터를 입력 받을 수 있는 상태인지 여부를 확인한다.(default : true) 
● getDoOutput(): Server에서 온 데이터를 출력 할수 있는 상태인지 여부를 확인한다.(default : false) 
● ifModifiedSince : 일부 프로토콜은 개체가 특정 시간보다 더 최근에 수정되지 않은 경우 개체 가져 오기 건너 뛰기를 지원한다. 0이 아닌 값은 GMT 1970 년 1 월 1 일 이후의 밀리 초 수로 시간을 제공한다. (default : 0) 
● useCaches : true프로토콜은 가능할 때마다 캐싱을 사용할 수 있다. 즉 이전에 다운로드 받은 데이터를 사용할지에대한 여부. DefaultUseCaches에서 가져 오며 기본값은 true이다.

 

▶ 1. HttpsURLConnection 사용 예제

1) 기본 예제

URL url = new URL("https://google.co.kr");
HttpsURLConnection httpsConn = (HttpsURLConnection) url.openConnection();
// 결과 Stream Data를 Stream객체에 할당하여 활용 가능하다.
httpsConn.getInputStream(); 

HttpUrlConnection 주요 설정
httpConn.setRequestMethod("GET"); //요청 방식 설정 (GET/POST 등)
httpConn.setRequestProperty("key", "value"); // request Header 설정 key-value 형식으로 다양한 요청 설정이 가능하다.
httpConn.setConnectTiomeOut(1000); //서버 연결 제한 시간
httpConn.setReadTimeOut(1000); // 서버 연결 후 데이터 read 제한 시간
//  URL 호출시 발생하는 무한 대기 상태에 빠지지 않도록 connectionTimeOut 발생시, ReadTimeOut 발생시 시간 설정을 꼭 해두자.

 - 아주 간단히는 위와 같은 방법으로 사용 가능하지만, URLConnection, 그리고 HttpsURLConnection을 적절히 사용하자면 다음과 같이 사용할 수 있을 것 같다.

 

ex) 구글

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;

public class Test {
	public static void main(String[] args) throws Exception {
		String urlString = "https://www.google.com";
		String line = null;
		InputStream in = null;
		BufferedReader reader = null; 
		HttpsURLConnection httpsConn = null;
		
		try {
			// Get HTTPS URL connection
			URL url = new URL(urlString);
			httpsConn = (HttpsURLConnection) url.openConnection();
			
			// Set Hostname verification
			httpsConn.setHostnameVerifier(new HostnameVerifier() {
				@Override
				public boolean verify(String hostname, SSLSession session) {
					// Ignore host name verification. It always returns true.
					return true;
				}
			});
			
			// Input setting
			httpsConn.setDoInput(true);
			// Output setting
			//httpsConn.setDoOutput(true);
			// Caches setting
			httpsConn.setUseCaches(false);
			// Read Timeout Setting
			httpsConn.setReadTimeout(1000);
			// Connection Timeout setting
			httpsConn.setConnectTimeout(1000);
			// Method Setting(GET/POST)
			httpsConn.setRequestMethod("GET");
			// Header Setting
			httpsConn.setRequestProperty("HeaderKey","HeaderValue");
			
			int responseCode = httpsConn.getResponseCode();
			System.out.println("응답코드 : " + responseCode);
			System.out.println("응답메세지 : " + httpsConn.getResponseMessage());
			
			// SSL setting
			SSLContext context = SSLContext.getInstance("TLS");
			context.init(null, null, null); // No validation for now
			httpsConn.setSSLSocketFactory(context.getSocketFactory());
			
			// Connect to host
			httpsConn.connect();
			httpsConn.setInstanceFollowRedirects(true);
			
			// Print response from host
			if (responseCode == HttpsURLConnection.HTTP_OK) { // 정상 호출 200
				in = httpsConn.getInputStream();
			} else { // 에러 발생
				in = httpsConn.getErrorStream();
			}
			reader = new BufferedReader(new InputStreamReader(in));
			while ((line = reader.readLine()) != null) {
				System.out.printf("%s\n", line);
			}
			
			reader.close();
		} catch (UnknownHostException e) {
			System.out.println("UnknownHostException : " + e);
		} catch (MalformedURLException e) {
			System.out.println(urlString + " is not a URL I understand");
        } catch (IOException e) {
        	System.out.println("IOException :" + e);
        } catch (Exception e) {
        	System.out.println("error : " + e);
        } finally {
            if (reader != null) {
            	reader.close();
            }
            if (httpsConn != null) {
                httpsConn.disconnect(); 
            }
        }
	}
}

 - 결과

응답코드 : 200
응답메세지 : OK
<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ko">
...

 

▶ 2. HttpsURLConnection 사용시 자주 발생하는 인증서 문제

 - httpsURLConnection을 사용하다보면 다음과 같은 인증서 문제를 많이 겪게 될 것이다.

 

1) 오류메세지 : SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: ...

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

 

2) 오류 발생 상황

 - SSL 인증서가 신뢰하는 기관 인증서가 없거나 SSL/TLS암호화 버전이 맞지 않는 경우 발생
 - 연결하려는 서버의 인증서가 신뢰하는 인증기관 인증서 목록(keystore)에 없을 경우 - 사설 인증서일 경우.
 - 서버/클라이언트 사이에 사용하려는 TLS 버전이 맞지 않을 때(TLS 1.0 만 지원하는 서버에 1.2로 hand shaking 요청등)
 - TLS 통신에 사용하려는 cipher suite 가 오래되거나 지원하지 않음. (JDK 1.8 부터는 sha1 지원 안되고 sha256 이상을 사용해야 한다고 한다.)

 

3) 해결 가이드 - 수동 SSL 인증서 등록 방법

 - http://www.javased.com/index.php?api=javax.net.ssl.TrustManager 참고

 - 다음 방법들 중 적용 가능한 방법을 선택하여 사용 하였다.

3.1) 시스템 프로퍼티 추가

System.setProperty("javax.net.ssl.keyStore",context.getKeyFile().getAbsolutePath());
System.setProperty("javax.net.ssl.keyStorePassword", context.getKeyFilePassword());
System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");

3.2) 공인 인증된 인증서로 서버 인증

 - InputStream에서 특정 CA를 가져와 그 CA를 사용해 KeyStore를 만든 후 이 KeyStore를 사용하여 TrustManager를 만들고 초기화합니다. 시스템에서는 하나 이상의 CA가 있는 KeyStore에서 TrustManager를 만들고 이를 사용하여 서버 인증서를 검증합니다.

(https://developer.android.com/training/articles/security-ssl?hl=ko#java) 참고

 - SSLContext.init() 함수에 인증서를 선언하여 호출 한다.

(기본적으로 [JRE 경로]/lib/security/cacerts 라는 파일명의 공인 인증된 인증서 저장소 파일을 사용하여 서버 인증서 검사가 가능 하다.)

KeyStore clientStore = KeyStore.getInstance("PKCS12");
clientStore.load(new FileInputStream("cert.p12"), "testPass".toCharArray());
 
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientStore, "pass".toCharArray());
KeyManager[] kms = kmf.getKeyManagers();
 
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream(System.getProperty("java.home") + "/lib/security/cacerts"), "changeit".toCharArray());

// Get Trust Manager  
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
TrustManager[] tms = tmf.getTrustManagers();
 
SSLContext sslContext = null;
sslContext = SSLContext.getInstance("TLS");
sslContext.init(kms, tms, new SecureRandom());
 
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

 

3.3) 아무 작업도 하지 않는 TrustManager를 설치하여 우회 하는 방법

 - 사실 많은 구글링을 통해 위와 같은 대안을 제시하는경우를 많이 보았다. 물론 나도 위와 같이 처리하여 임시로 인증서 오류를 우회하여 처리하기도 하였다.

 - 이와 같이 처리하는 경우 보안 이슈도 있으며, 앱의 경우 배포 리젝도 발생할 수 있으니 조심 하도록 하자. 사용하는 방법은 매우 간단하다.

 - 다음 내용을 추가해주면 된다.

TrustManager[] trustAllCerts = 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) {
	}
} };

SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

 

ex) naver.com 실습

 - 네이버와 같은 경우 위의 예시를 그대로 사용하였을 경우 인증서 오류가 발생하는 것을 볼 수 있다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;

public class Test {
	public static void main(String[] args) throws Exception {
		String urlString = "https://www.naver.com/";
		String line = null;
		InputStream in = null;
		BufferedReader reader = null; 
		HttpsURLConnection httpsConn = null;
		
		try {
			// Get HTTPS URL connection
			URL url = new URL(urlString);
			httpsConn = (HttpsURLConnection) url.openConnection();
			
			// Set Hostname verification
			httpsConn.setHostnameVerifier(new HostnameVerifier() {
				@Override
				public boolean verify(String hostname, SSLSession session) {
					// Ignore host name verification. It always returns true.
					return true;
				}
			});
			
			// Input setting
			httpsConn.setDoInput(true);
			// Output setting
			//httpsConn.setDoOutput(true);
			// Caches setting
			httpsConn.setUseCaches(false);
			// Read Timeout Setting
			httpsConn.setReadTimeout(1000);
			// Connection Timeout setting
			httpsConn.setConnectTimeout(1000);
			// Method Setting(GET/POST)
			httpsConn.setRequestMethod("GET");
			// Header Setting
			httpsConn.setRequestProperty("HeaderKey","HeaderValue");
			
			int responseCode = httpsConn.getResponseCode();
			System.out.println("응답코드 : " + responseCode);
			System.out.println("응답메세지 : " + httpsConn.getResponseMessage());
			
			// SSL setting
			SSLContext context = SSLContext.getInstance("TLS");
			context.init(null, null, null); // No validation for now
			httpsConn.setSSLSocketFactory(context.getSocketFactory());
			
			// Connect to host
			httpsConn.connect();
			httpsConn.setInstanceFollowRedirects(true);
			
			// Print response from host
			if (responseCode == HttpsURLConnection.HTTP_OK) { // 정상 호출 200
				in = httpsConn.getInputStream();
			} else { // 에러 발생
				in = httpsConn.getErrorStream();
			}
			reader = new BufferedReader(new InputStreamReader(in));
			while ((line = reader.readLine()) != null) {
				System.out.printf("%s\n", line);
			}
			
			reader.close();
		} catch (UnknownHostException e) {
			System.out.println("UnknownHostException : " + e);
		} catch (MalformedURLException e) {
			System.out.println(urlString + " is not a URL I understand");
        } catch (IOException e) {
        	System.out.println("IOException :" + e);
        } catch (Exception e) {
        	System.out.println("error : " + e);
        } finally {
            if (reader != null) {
            	reader.close();
            }
            if (httpsConn != null) {
                httpsConn.disconnect(); 
            }
        }
	}
}

 - 결과 

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: 
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

 - 3.3 적용하여 호출시

String urlString = "https://www.naver.com/";
String line = null;
InputStream in = null;
BufferedReader reader = null; 
HttpsURLConnection httpsConn = null;

try {
	// Get HTTPS URL connection
	URL url = new URL(urlString);
	httpsConn = (HttpsURLConnection) url.openConnection();
	
	// Set Hostname verification
	httpsConn.setHostnameVerifier(new HostnameVerifier() {
		@Override
		public boolean verify(String hostname, SSLSession session) {
			// Ignore host name verification. It always returns true.
			return true;
		}
	});
	
	TrustManager[] trustAllCerts = 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) {
		}
	} };

	SSLContext sc = SSLContext.getInstance("SSL");
	sc.init(null, trustAllCerts, new java.security.SecureRandom());
	HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
	
	// Input setting
	httpsConn.setDoInput(true);
	// Output setting
	//httpsConn.setDoOutput(true);
	// Caches setting
	httpsConn.setUseCaches(false);
	// Read Timeout Setting
	httpsConn.setReadTimeout(1000);
	// Connection Timeout setting
	httpsConn.setConnectTimeout(1000);
	// Method Setting(GET/POST)
	httpsConn.setRequestMethod("GET");
	// Header Setting
	httpsConn.setRequestProperty("HeaderKey","HeaderValue");
	
	int responseCode = httpsConn.getResponseCode();
	System.out.println("응답코드 : " + responseCode);
	System.out.println("응답메세지 : " + httpsConn.getResponseMessage());
	
	// SSL setting
	SSLContext context = SSLContext.getInstance("TLS");
	context.init(null, null, null); // No validation for now
	httpsConn.setSSLSocketFactory(context.getSocketFactory());
	
	// Connect to host
	httpsConn.connect();
	httpsConn.setInstanceFollowRedirects(true);
	
	// Print response from host
	if (responseCode == HttpsURLConnection.HTTP_OK) { // 정상 호출 200
		in = httpsConn.getInputStream();
	} else { // 에러 발생
		in = httpsConn.getErrorStream();
	}
	reader = new BufferedReader(new InputStreamReader(in));
	int rank = 0;
	while ((line = reader.readLine()) != null) {
		 System.out.printf("%s\n", line); 
	}
	
	reader.close();
} catch (UnknownHostException e) {
	System.out.println("UnknownHostException : " + e);
} catch (MalformedURLException e) {
	System.out.println(urlString + " is not a URL I understand");
} catch (IOException e) {
	System.out.println("IOException :" + e);
} catch (Exception e) {
	System.out.println("error : " + e);
} finally {
	if (reader != null) {
		reader.close();
	}
	if (httpsConn != null) {
		httpsConn.disconnect(); 
	}
}

 - 결과

응답코드 : 200
응답메세지 : OK

<!doctype html>                <html lang="ko" data-dark="false"> <head> <meta charset="utf-8"> <title>NAVER</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=1190"> <meta name="apple-mobile-web-app-title" content="NAVER"/> <meta name="robots" content="index,nofollow"/> <meta name="description" content="네이버 메인에서 다양한 정보와 유용한 컨텐츠를 만나 보세요"/> <meta property="og:title" content="네이버"> <meta property="og:url" content="https://www.naver.com/"> <meta property="og:image" content="https://s.pstatic.net/static/www/mobile/edit/2016/0705/mobile_212852414260.png"> <meta property="og:description" content="네이버 메인에서 다양한 정보와 유용한 컨텐츠를 만나 보세요"/> <meta name="twitter:card" content="summary"> <meta name="twitter:title" content=""> <meta name="twitter:url" content="https://www.naver.com/"> <meta name="twitter:image" content="https://s.pstatic.net/static/www/mobile/edit/2016/0705/mobile_212852414260.png"> <meta name="twitter:description" content="네이버 메인에서 다양한 정보와 유용한 컨텐츠를 만나 보세요"/>  <link rel="stylesheet" href="https://pm.pstatic.net/dist/css/nmain.20200806.css"> <link rel="stylesheet" href="https://ssl.pstatic.net/sstatic/search/pc/css/api_atcmp_200709.css"> <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?1"/>   <script type="text/javascript" src="https://pm.pstatic.net/dist/lib/nelo.20200617.js" defer="defer"></script> <script>document.domain="naver.com",window.nmain=window.nmain||{},window.nmain.supportFlicking=!1;var nsc="navertop.v4",ua=navigator.userAgent;window.nmain.isIE=navigator.appName&&0<navigator.appName.indexOf("Explorer")&&ua.toLocaleLowerCase().indexOf("msie 10.0")<0,document.getElementsByTagName("html")[0].setAttribute("data-useragent",ua),window.nmain.isIE&&(Object.create=function(n){function a(){}return a.prototype=n,new a})</script> <script>var darkmode= false;window.naver_corp_da=window.naver_corp_da||{main:{}},window.naver_corp_da.main=window.naver_corp_da.main||{},window.naver_corp_da.main.darkmode=darkmode</script> <script> window.nmain.gv = {  isLogin: false,
...

 - 정상 응답이 온 것을 볼 수 있다.

 - 왠만하면 올바른 인증서를 사용하여 인증하도록 하자.

반응형

댓글을 달아 주세요

  1. 경민 2021.01.06 09:06

    감사합니다. 문제해결에 많은도움 되었습니다.

  2. Favicon of https://jeg-dev.tistory.com BlogIcon 노력은배신안함 2021.09.21 23:33 신고

    안녕하세요, 대희님 좋은자료 정말 감사드립니다. 다름이 아니라 TLS통신 관련하여서 여쭤보고 싶은 것이 있어 댓글로 글남기게 되었습니다.
    앱에서 검출된 TLS1.0으로 요청 시 취약점으로 인하여, 앱에서 TLS1.2v로 변경하여 저희 서버로 요청을하고, 기존 WEB서버 (nginx) 설정은, 이미 TLS1.2v 허용 가능하게 끔 설정 되어있는상황입니다. 저희 서버가nginx <-> jboss 연동하여 web-was를 두고 사용 중인 상황인데... 기존에 저희 웹쪽으로 찌르고 들어오는 것 말고, 저희 서버와 상대 서버 간의 WAS <-> WAS 통신 하고 있는 사항도, 고려 해야 하는 부분일까요..? nginx설정에서 TLS1.0v지원기능을 빼버리게되면 다른 클라이언트에서 저희 웹서버로 들어오는건 규격이 맞지않으면 문제가 되겠지만, 저희 서버 즉 was자체에서 (방화벽 연동 후) http요청을 보내서 다른서버 API를 콜하는 것인데 혹시 이것도 TLS통신이라고 볼수있는걸까요..!? 아시는 부분이 있으시다면, 도움 주시면 감사드리겠습니다 ㅠㅠ!