해당 포스트는 "안드로이드 통신+보안 프로그래밍" 책의 내용을 요약한 것이다.



: 앞 포스트에서 URL 클래스는 엔티티이외의 정보를 읽을 수 없었다. 따라서 URL은 요청 메시지를 만드는 데 최소한의 요청 URI를 구성하기 위해 사용한다. URLConnection, HttpURLConnection 클래스를 사용해서 요청 메시지를 작성하고 응답 메시지를 해석하는 데 사용한다.


※ URLConnection 클래스 

: 사용자 인증이나 보안이 설정되어 있지 않은 웹서버에 접속하여 파일 등을 다운로드하는데 많이 사용한다.


- URLConnection 클래스 사용 방법

1. URL 객체를 생성하고 URLConnecton 객체를 만든다.

String parameter = URLEncoder.encode("name", "UTF-8") + "=" + URLEncoder.encode(name, "UTF-8"); parameter += "&" + URLEncoder.encode("age", "UTF-8") + "=" + URLEncoder.encode("45", "UTF-8"); URL url = new URL("http://www.example.com/path.html?"+parameter); URLConnection con = url.openConnection()

2. HTTP 헤더를 작성한다.

con.setRequestProperty("Accept-Language","ko-kr,ko;q=0.8"+"en-us;q=0.5,en;q=0.3");

3. setConnectTimeout() 메서드로 타임 아웃 시간을 설정한다.

4. setDoOutput()과 setDoInput() 메서드를 사용하여 인티티의 입력 또는 출력 여부를 결정한다.

: URLConnection 내부에는 boolean 타입의 doInput과 doOutput 필드가 있다. setDoOutput()/setDoInput()을 하면 데이터를 출력 또는 입력 할 수 있는 권한이 생긴다. 그러면 Output(Input)Stream 객체를 생성해 서버와 통신할 수 있다. Output의 경우 디폴트가 false고 Input의 경우 true이다.

5. 요청 메시지에 엔티티를 만들어야 하면 OutputStream 객체를 이용해 제작한다.

OutputStreamWriter wr = new OutputStreamWriter(con.getOutputStream());
wr.write(parameter);
wr.flush();

6. 서버로부터 받은 응답 메시지에서 데이터를 추출해 자바의 문자열로 변환한다.

BufferedReader re = new BufferedReader(new InputStreamReader(con.getInputStream(),"UTF-8"));
String line = null;
while((line=rd.readLine())!=null){
	//로직 처리
}

참고로 con.getHeaderField("Set-Cookie");로 헤더의 내용을 읽을 수 있다.

7. 작업이 끝나면 close로 자원을 해제시킨다.


ex) 이전 포스트의 URLReader 예를 URLConnection 클래스를 사용해서 구현했다.

public class URLReader {
	public static void main(String[] args) throws Exception{
		URL google = new URL("http://www.naver.com/");
		URLConnection con = google.openConnection();
		con.connect();
		
		BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
		String line;
		while((line = in.readLine())!=null)
			System.out.println(line);
		in.close();
	}
}


- URLConnection 메서드

1. void addRequesProperty(String key, String value) : 키와 값의 쌍으로 요청 메시지의 헤더 필드를 추가한다.

2. Map<String, List<String>> getHeaderFields() : 헤더의 필드들을 Map 객체로 반환한다.

3. String getContentType() : 응답 메시지의 Content-Type에 명시된 MIME 타입을 반환한다. 해당 메서드는 getContent(리소스를 객체로 만들어  반환한다) 메서드를 사용하기 전에 객체 타입을 확인하는 데 사용할 수 있다.

4. void setConnectTimeout(int timeout) : getContent()나 getInputStream()을 사용하여 서버와 연결 시 타임아웃 시간을 설정한다.

* 참고로, getInputStream() 메서드를 호출해 입력 스트림 객체를 만들 때 내부에서 connect() 메서드를 자동으로 실행한다. 따로 connect() 메서드를 호출할 필요가 없다.


※ HttpURLConnection 클래스 

: 웹 서버와 통신할 떄 URLConnection 클래스보다 HttpURLConnection 클래스를 사용하는 게 더 편리하다. 


- HttpURLConnection 메서드

1. void setFollowRedirects(boolean auto) : HTTP 리다이렉트(300) 따를 건지 설정한다. 디폴트는 true이다.

2. void setRequestMethod(String method) : 요청 메서드(get,put,delete)를 설정한다.

3. int getResponseCode() : 응답 메시지의 상태 코드를 반환한다.

4. String getResponseMessage() : 응답 메시지 상태라인에 있는 이유 문구를 반환한다.(ex. "OK", "Not Found")

5. InputStream getErrorStream() : 서버와 연결하는 과정에서 에러가 나면 에러 메시지를 받는 데 이를 읽을 InputStream 객체를 반환한다.

6. void disconnect() : keepAlive 헤더를 false로 만든다. 서버와 한 번 연결하면 자동으로 연결이 끊긴다. 서버와 자주 연결하는 환경에서는 좋지 않다.


※ HTTP 쿠키

: 웹서버에서 클라이언트와의 요청과 응답 상태를 지속적으로 관리할 필요가 있는 데 이러한 상태 정보를 쿠키를 통해 저장한다. 클라이언트가 서버에게 요청 메시지를 전송하면 서버는 클라이언트에게 "Set-Cookie:Name=value"형태로 쿠키를 저장하라고 쿠키를 준다. 그러면 클라이언트는 쿠키를 "Cookie:name=value" 형태로 요청 메시지에 첨부하여 서버에 전송한다. 쿠키의 속성에는 작업 종료 시 삭제여부(Discard), 도메인, 쿠키 만료일, 쿠키가 적용되는 URI 경로, 응답 메시지에 포함되는 포트, 보안 유무, 버전 등이 있다. 쿠키 정보를 획득할 때는 CookieHandler와 CookieManager 클래스를 사용한다. CookieHandler는 URL별로 응답 메시지에서 쿠키를 분리하고 요청 메시지에 쿠키를 첨부하는 기능을 한다. CookieManager는 쿠키로부터 필요한 정보를 객체 내부에 저장하는 기능을 수행한다. 해당 클래스를 사용하는 방법은 대략 다음과 같다.

CookieHandler.setDefault(new CookieManager(new MyCookieStore(),new MyCookiePolicy()));
또는 
CookieHandler.setDefault(new CookieManager());
((CookieManager)CookieHandler.getDefault()).setCookiePolicy(new MyCookiePolicy());

java.net.CookieManager manager = new java.net.CookieManager();
manager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
CookieHandler.setDefault(manager);

CookieStore cookieJar = manager.getCookieStore();
List<HttpCookie> cookies = cookieJar.getCookies();
for(HttpCookie c : cookies){
	// 쿠키 얻음
}

위 코드에서 ACCEPT_ALL 는 모든 쿠키를 받아들이고 ACCEPT_NONE은 쿠키를 보관하거나 관리하지 않는다. ACCEPT_ORIGINAL_SERVER는 서버에서 제공되는 쿠키만을 보관하고 관리한다.



※ HTTP 사용자 인증

- 인증(Authentication)

: 사용자가 이전에 등록한 사용자인지 판단하는 과정이다. 사용자는 서버로부터 자신을 정당한 사용자로 인증을 받아야 서버에 특정 리소스에 접근하거나 데이터 수정을 할 수 있다. 대개 인터넷은 아이디와 패스워드 로그인으로 인증을 한다. 

 

- 권한 부여(Authorization)

: 사용자가 서버에 인증을 받으면 특정 리소스에 접근하거나 데이터 수정, 입력할 수 있는 권한이 부여된다.


- HTTP 사용자 기본 접근 인증
: 아이디, 패스워드 기반으로 구성된 인증 방법으로 간단하기 때문에 보안 공격에 취약할 수 있다. HTTP 기본 접근 인증의 작업 순서는 다음과 같다.
1. 클라이언트가 요청 메시지를 작성하고 서버에 메시지를 전송한다. 
2. 요청한 데이터가 인증이 필요하다면 서버는 응답 메시지(401, Authorization Required Response)로 인증이 필요하다고 클라이언트에게 알린다. 응답 메시지 헤더에는 WWW-Authenticate: Basic realm="File Download Authorization"이 포함된다. Basic은 기본 접근 인증을 의미한다.
3. 사용자는 아이디와 패스워드 BASE64로 암호화해 요청 메시지에 넣어 서버에 전송한다. 요청 메시지 헤더에는 Authorization: Basic (아이디:패스워드 BASE64 암호화) 가 들어간다. 
4. 아이디와 패스워드가 유효하다면 사용자가 요청한 데이터를 엔티티에 넣어 응답 메시지를 보내고 유효하지 않다면 응답 메시지 401을 다시 보낸다.

- 안드로이드/자바에서 사용자 기본 접근 인증 절차
1. URL을 생성하고 아이디와 패스워드를 입력 받는다.
2. String userPassword = userName+":"+password; 로 아이디와 패스워드를 하나의 String으로 만든다.
3. String encoding = Base64.encodeBase64(userPassword.getBytes("UTF-8")); 로 문자열을 인코딩한다.
4. URLConnection uc = url.openConnection(); 으로 URLConnection 객체를 생성한다.
5. uc.add(set)RequestProperty("Authorization","Basic"+encoding); 으로 요청 메시지 헤더 내에 추가한다.
: 기본 접근 인증은 간단하고 언제든지 HTTP 헤더에 데이터를 넣어서 인증할 수 있다는 장점이 있지만 보안에 많이 취약하다. 따라서 HTTPS 프로토콜을 사용해 암호화하는 게 좋다. 기본 접근 인증의 대표적인 예는 집에서 사용하는 공유기이다. 대부분 인터넷에서 운영중인 서버는 보통 HTTPS 프로토콜을 사용하고 사용자 인증을 위해 OAuth2.0 인증을 이용한다. 또한 공개키 구조와 보안 소켓 레이어를 사용해 전송하는 메시지를 중간에 가로챌 수 없다. 그 대신 속도가 지연되는 단점도 있다. 

* HttpURLConnection 객체는 내부적으로 버퍼를 관리하지 않는다. 따라서 BufferedInput(Output)Stream을 사용하는 게 좋다.
* 서버로 데이터를 업로드할 때 원칙적으로 엔티티의 크기를 산정해서 헤더에 설정해 줘야 한다.
con.setRequestProperty("Content-Length",String.valueOf(contentLength));

자바 1.5이상 부터는 직접 헤더를 안 만들어도 된다. void setFixedLengthStreamingMode(int contentLength) 메서드를 이용해 헤더를 만들 수 있다. 하지만 전송할 데이터 크기는 사용자가 계산해야 한다. 또한 엔티티를 정크로 만들면 사용자가 별도로 전송할 데이터의 크기를 알 필요가 없는 장점이 있다. void setChunkedStreamingMode(int chunklen)에 0이나 음수 매개변수를 주면 데이터의 크기를 별도로 명시 안 해도 된다.

 

ex) 안드로이드 사용자 인증 예제


public class ConnectionActivity extends Activity {
 private Handler  mHandler;
 final Activity activity = this;
 private static final int MSG_ID = 1;
 private static final String username = "admin";
 private static final String password = "admin";
 private final String TAG = "BasicAuthentication";
 @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final EditText eText = (EditText) findViewById(R.id.address);
        final Button button = (Button) findViewById(R.id.ButtonGo);
        final TextView text = (TextView) findViewById(R.id.pagetext);
        mHandler = new Handler() {
         @Override
      public void handleMessage(Message msg) {
         switch (msg.what) {
            case MSG_ID:
             String s = (String) msg.obj;
             text.setText(s);
               break;
         }
            super.handleMessage(msg);
         }
      };
        button.setOnClickListener(new Button.OnClickListener() {
            @Override
   public void onClick(View v) {
             mThread t = new mThread(eText.getText().toString());
             t.start();
            }
        });
    }
    private class mThread extends Thread {
     private final String s;
     public mThread(String s) {
      this.s = s;
     }
     @Override
     public void run() {
      if ( !dowloadUrl(s)) {
       Message m = new Message();
       m.what = MSG_ID;
       m.obj = ("DownLoad 작업을 시작하지 못했습니다.");
       mHandler.sendMessage(m);
      }
     }
       private Boolean dowloadUrl(String surl) {
         URL url;
       try {
     url = new URL(surl);
    } catch (MalformedURLException e) {
     e.printStackTrace();
     return false;
    }
    try {
    HttpURLConnection con = (HttpURLConnection) url.openConnection();
                   con.setRequestMethod("GET");
    con.setRequestProperty("Accept-Language", "ko,en-US;q=0.7,en;q=0.3");
                  
    //서버가 GZIP으로 리소스를 제공하는 경우 안드로이드 5.0버전에
              //버그가 존재하여 인위적으로 Accept-Encoding을 넣어야 한다.
    con.setRequestProperty("Accept-Encoding", "" );
     
    //사용자 인증을 위해 Authorization 헤더 데이터를 만든다.
    String authString = username + ":" + password;
     byte[] authEncBytes = Base64.encode(authString.getBytes(),Base64.NO_WRAP);
     String authStringEnc = new String(authEncBytes);
     Log.v(TAG,"Basic " + authStringEnc);
     
     con.setRequestProperty("Authorization", "Basic " + authStringEnc);
    con.setConnectTimeout(3000);
    con.setReadTimeout(3000);
    //서버와의 연결을 요청한다.
    con.connect();
    int status = con.getResponseCode();
    switch(status) {
         case HttpURLConnection.HTTP_OK : {
          InputStream is = con.getInputStream();
          String encode =  con.getContentEncoding();
      //서버에서 클라이언트가 요청한 데이터를 gzip으로 압축해서 보냈다면
          if (encode.equalsIgnoreCase("gzip")){
           is = new GZIPInputStream(is);
          }
         String mimeType = con.getContentType();
         String charset = null;
         String[] values = mimeType.split(";");
         for (String value : values) {
          value = value.trim();
          if (value.toLowerCase().startsWith("charset=")) {
             charset = value.substring("charset=".length());
          }
         }
         if (charset == null)
          charset = "UTF-8";
      //엔티티 헤더의 Charset을 이용해 문자를 디코딩한다.
         BufferedReader reader = new BufferedReader(new InputStreamReader(is,charset));
         StringBuilder s = new StringBuilder();
         String line;
         while((line = reader.readLine()) != null) {
          s.append(line);
         }
         Message m2 = new Message();
         m2.what = MSG_ID;
         m2.obj = s.toString();
         mHandler.sendMessage(m2);
         reader.close();
         }
    }
     } catch (Exception e){
      e.getStackTrace();
      return false;
      }
      return true;
     }
    }
}

추가로 자바 6.0에서 Authenticator 클래스를 사용하면 Authorization 헤더를 자동으로 생성할 수 있다.

Authenticator.setDefault(new Authenticator(){
 @Override
 protected PasswordAuthentication getPasswordAuthentication(){
  String name="admin";
  String password="admin";
  return new PasswordAuthentication(name, password.toCharArray());
 }
}  

+ Recent posts