RestTemplate, HttpURLConnection, Parameter, URL, 그리고 Base64 인코딩 이슈

dropKick·2022년 1월 10일
0

공부 기록

목록 보기
48/67

REST API 형식의 AES256 과제를 수행 중 발생한 이슈들

인코딩 이슈

단순히 Request URL 파라미터를 던질 시 발생하는 인코딩 이슈
인코딩 이슈가 발생하는 기본적인 인코딩 방법에 대해서 알아보고 발생한 이슈를 해결한 방법을 알아봄

파라미터 인코딩

  • 파라미터 인코딩 링크

  • 기본적으로 웹 브라우저/클라이언트에서 웹 서버로 단순한 GET 요청이 아닌 POST의 경우 이에 대한 파라미터(Request Parameter)를 보내고 해석하기 위해선 브라우저와 서버가 동일한 charset 인코딩 방법을 사용해야한다.
    따라서 jsp에서는 <%@ page contentType="text/html; charset="EUC-KR" %>와 같은 방식으로 jsp가 응답 할(서버가 렌더링 할) charset을 명시하고, 브라우저는 이에 맞춘 charset으로 데이터를 전송한다.
    또한, 별도로 각 Http request/response 헤더에 타입을 명시하여 인코딩한다.

URL 인코딩

  • URL 인코딩 링크
  • URL의 전송은 ASCII 코드로밖에 사용할 수 없음
    이를 위해서 String URL은 Base64로 인코딩 됨

Base64 인코딩

  • ASCII 코드 변환을 위한 인코딩 방법
  • 데이터를 아스키 코드 대응 후 8bit씩 끊어 적용 후 10진수 6비트 변환, 비는 부분은 0(Zero) 패딩
  • 유의점으로 Base64 인코딩의 62/63이 '+' 와 '/'로 각 URL 인코딩에서 사용하는 문자이기때문에 이 문자를 '-'와 '_'로 변환해주는 Base64 URL Safe가 나왔다.

이슈

1. 스프링 RestTemplate의 경우

  • URL Obejct의 사용
    URL Object
    URL Object

  • String URL UTF-8 인코딩/디코딩

    • Request의 경우 org.springframework.web.util.DefaultUriTemplateHandler을 사용해서 자동 인코딩
    • Response의 경우 org.springframework.http.converter.StringHttpMessageConverter을 사용하여 자동 인코딩
    • 만약 EUC-KR 등의 다른 인코딩 방법을 사용해야하는 경우 ResponseEntity.xxForEntity(accessURL, class)에 의해 UTF-8로 재인코딩이 되지 않도록 URI 객체를 넘겨야한다.

RestTemplate 적용, application/json 통신

별도의 인코딩을 적용하지 않았음에도 자동 인코딩이 적용되어 정상 통신

클라이언트
public static void MyrestTemplatePost() throws NoSuchElementException, UnsupportedEncodingException, GeneralSecurityException {
	
	// RestTemplate
	RestTemplate restTemplate = new RestTemplate();
		
	// Header 설정
	HttpHeaders httpHeaders = new HttpHeaders();
	httpHeaders.setContentType(MediaType.APPLICATION_JSON); // RequestBody를 통해 서버가 받을 MIME
	httpHeaders.setAccept(Collections.singletonList(MediaType.valueOf("application/json; charset=UTF-8"))); // ResponseBody를 통해 내가 받을 MIME
		
	/*
	 * HttpEntity 설정
	 */
	HttpEntity<JSONObject> httpEntity = new HttpEntity<JSONObject>(jsonObject, httpHeaders);
		
	System.out.println("Client Send Entity" + httpEntity);

	/*
	 * ResponseEntity 설정
	 * accessURL, Request Object, Response Type, HTTP Header
	 */
	ResponseEntity<String> responseEntity = restTemplate.postForEntity(apiURL, httpEntity, String.class, httpHeaders);
		
	System.out.println("Server Response Entity: " +  responseEntity.toString());
}
서버
@RequestMapping(value = "rest", method = RequestMethod.POST)
@ResponseBody
public static ResponseEntity<String> restTemplatePost(@RequestBody JSONObject jsonObject) throws NoSuchElementException,
			UnsupportedEncodingException, GeneralSecurityException, JSONException {

	ResponseEntity<String> responseEntity = null;

	System.out.println("Client Data: " + jsonObject);

	HttpHeaders responseHeaders = new HttpHeaders();
	responseHeaders.add("Content-Type", "application/json; charset=UTF-8");

	responseEntity = new ResponseEntity<String>(jsonObject.toJSONString(), responseHeaders, HttpStatus.OK);
	System.out.println("Server Respon Entity: " + responseEntity);

	return responseEntity;
}
통신 결과
  • 클라이언트

    Client Send Entity: 
    <{"phoneNo":"OAP1jPHVcsiZIcz\/f5YARg==","type":"type1","socialNo":"wU6VVLaZN0Moq+4nOyVEVw=="},{Content-Type=[application/json], Accept=[application/json;charset=UTF-8]}>
    
    Server Response Entity: 
    <200 OK,{"phoneNo":"0123456789","resultMSG":"SUCCESS","type":"type1","socialNo":"1234567","resultCD":"0000"},{Server=[Apache-Coyote/1.1], Content-Type=[application/json;charset=UTF-8], Content-Length=[100], Date=[Mon, 10 Jan 2022 02:15:32 GMT]}>
    
  • 서버

    Client Send Data: {"phoneNo":"OAP1jPHVcsiZIcz\/f5YARg==","type":"type1","socialNo":"wU6VVLaZN0Moq+4nOyVEVw=="}
    
    Server Respon Entity: 
    <200 OK,{"phoneNo":"0123456789","resultMSG":"SUCCESS","type":"type1","socialNo":"1234567","resultCD":"0000"},{Content-Type=[application/json; charset=UTF-8]}>
    

2. x-www-form-urlencoded 인코딩 명시

  • URL을 통해 파라미터를 전송해야한다면 x-form-urlencoded를 사용
  • RequestBody로 데이터가 전달되지 않음
  • x-www-form-urlencoded의 경우 각 라이브러리나 프레임워크에서 자동 인코딩을 구현

3. URL Encoding 패딩 제거

  • URL 인코딩의 경우 안전한 ASCII 코드 변환을 위해 Base64 인코딩을 사용하지만 Base64 인코딩으로 인해 제로 패딩이 발생

클라이언트의 경우

SEND JSON: {"phoneNo":"OAP1jPHVcsiZIcz\/f5YARg==","type":"type1","socialNo":"wU6VVLaZN0Moq+4nOyVEVw=="}

URL Encode: %7B%22phoneNo%22%3A%22OAP1jPHVcsiZIcz%5C%2Ff5YARg%3D%3D%22%2C%22type%22%3A%22type1%22%2C%22socialNo%22%3A%22wU6VVLaZN0Moq%2B4nOyVEVw%3D%3D%22%7D

RESPONSE JSON: {"resultMsg":"success","phoneNo":"0123456789","type":"type1","socialNo":"1234567","resultCD":"0000"}

서버의 경우

  • 마지막 패딩 발생
    별도의 패딩 처리를 하지 않을 시 예외 발생
threw exception [Request processing failed; 
nested exception is Unexpected character (=) 
at position 92.] with root cause
Unexpected character (=) at position 92.
Client Http Conn Data: 
%7B%22phoneNo%22%3A%22OAP1jPHVcsiZIcz%5C%2Ff5YARg%3D%3D%22%2C%22type%22%3A%22type1%22%2C%22socialNo%22%3A%22wU6VVLaZN0Moq%2B4nOyVEVw%3D%3D%22%7D=

URL Decode String: {"phoneNo":"OAP1jPHVcsiZIcz\/f5YARg==","type":"type1","socialNo":"wU6VVLaZN0Moq+4nOyVEVw=="}

RESPONSE JSON: {"resultMsg":"success","phoneNo":"0123456789","type":"type1","socialNo":"1234567","resultCD":"0000"}

4. HttpURLConnection에서의 URL Encode

  • HttpURLConnection은 RestTemplate과 달리 별도의 인코딩 로직이 존재하지 않음. 인코딩을 하지 않고 URL을 통해 데이터를 보낼 시 에러 발생

URL 인코딩 X

  • 클라이언트가 보낸 데이터{"phoneNo":"OAP1jPHVcsiZIcz\/f5YARg==","type":"type1","socialNo":"wU6VVLaZN0Moq+4nOyVEVw=="}

  • 서버가 받은 데이터
    %7B%22phoneNo%22%3A%22OAP1jPHVcsiZIcz%5C%2Ff5YARg=%3D%22%2C%22type%22%3A%22type1%22%2C%22socialNo%22%3A%22wU6VVLaZN0Moq+4nOyVEVw%3D%3D%22%7D

  • 디코딩 후
    {"phoneNo":"OAP1jPHVcsiZIcz\/f5YARg==","type":"type1","socialNo":"wU6VVLaZN0Moq 4nOyVEVw=="}

  • POST 데이터 파라미터 인코딩의 경우 '+'가 공백을 의미하기에 공백 치환 후 공백을 암호화할 수 없음. 공백 암호화 예외 발생

ception: Given final block not properly padded] with root cause
javax.crypto.BadPaddingException: Given final block not properly padded 

URL 인코딩 사용

따라서 URL 인코딩을 사용하기 위한 java.lang.urlEncoder.encode()/decode()에는 다음과 같은 로직이 포함

  • java.net.URLEncoder.encode() '+'의 공백문자 치환
    public static String encode(String s, String enc) 
	throws UnsupportedEncodingException {

	for (int i = 0; i < s.length();) {
	    int c = (int) s.charAt(i);;
	    if (dontNeedEncoding.get(c)) {
		if (c == ' ') {
		    c = '+';
		    needToChange = true;
		}
  • java.net.URLDecoder.decode 공백문자의 '+' 치환
    public static String decode(String s, String enc) 
	throws UnsupportedEncodingException{

	char c;
	byte[] bytes = null;
	while (i < numChars) {
            c = s.charAt(i);
            switch (c) {
	    case '+':
		sb.append(' ');
  • 원래 있던 '+' 문자의 경우는?
    URLEncode.encode에 의해 '%2B'로 ASCII 변환

    • "socialNo":"wU6VVLaZN0Moq+4nOyVEVw=="}

    • socialNo%22%3A%22wU6VVLaZN0Moq%2B4nOyVEVw%3D%3D%22%7D

번외. Request Mapping java.lang.IllegalArguemntException: argument type mismatch

데이터가 자바 오브젝트가 아닐 때 발생하는 이슈

RequestMapping을 위해선 클라이언트가 전송한 데이터를 자바 Object로 바인딩시켜야하는데 그렇지 못했을 때 발생
[0][type=org.springframework.validation.support.BindingAwareModuleMap] [value={}]

해결법

  • RequestMapping 메소드에 @RequestBody 어노테이션 적용

  • @RequestBody는 Client가 전송하는 Json(application/json) 형태의 HTTP Body 내용을 Java Object로 Binding 시켜주는 역할
    @RequestBody로 받는 Data는 Spring에서 관리하는 MessageConverter들 중 하나인 MappingJackson2HttpMessageConverte를 통해 Java Object로 Binding
    @RequestBody는 Setter함수가 없어도 요청받은 데이터를 Binding 가능

결론

  • String URL의 경우 URL 인코딩을 적용 이 때 공백처리에 유의, 하지만 Base64URLSafe를 사용한다면 '+'는 '-', '/'를 각각 '-', '_'로 바꿔주어 별도의 처리 없이 사용가능
  • RestTemplate 사용 시 RestTemplate.xxForEntity에 의한 기본 인코딩 방식이 존재함에 유의
  • 바이너리 데이터가 아닌 데이터를 URL 전송 시 x-www-form-urlencoded를 이용
  • URL 인코딩에 의한 패딩 유의

0개의 댓글