공공 데이터 Open API 삽질기
공공 데이터 Open API 에서 Service Key 인코딩 문제 해결기
문제 코드
// Web 요청
final RestTemplate restTemplate = new RestTemplate();
final String url = "<http://apis.data.go.kr/6410000/busrouteservice/getBusRouteInfoItem>";
final String serviceKey = "QIMPxKy%2B%2FltHKSomethingapikeyxg%3D%3D";
final UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
.queryParam("serviceKey", serviceKey)
.queryParam("routeId", "234000031");
final String requestUri = builder.toUriString();
""
// OpenAPI 호출
ResponseEntity<String> response = restTemplate.getForEntity(requestUri, String.class);
System.out.println(response.getBody());
result
<OpenAPI_ServiceResponse>
<cmmMsgHeader>
<errMsg>SERVICE ERROR</errMsg>
<returnAuthMsg>SERVICE_KEY_IS_NOT_REGISTERED_ERROR</returnAuthMsg>
<returnReasonCode>30</returnReasonCode>
</cmmMsgHeader>
</OpenAPI_ServiceResponse>
문제
정상적으로 serviceKey
를 입력했음에도, serviceKey
가 등록되지 않았다는 오류를 반환받음
분석
request URL 변환 과정에 문제가 있다고 가정하고 debug 진행
query 를 간편하게 삽입하기 위해서 UriComponentsBuilder
를 활용하였는데, %
은 unsafe character
로 URI 변환 과정에서 %
이 %25
로 변환된 모습을 볼 수 있었음
URIComponentBuilder
URIComponentBuilder
의 결과를 toUriString()
할 때를 찾아보면 다음을 호출한다.
this.build().encode().toUriString()
encode()
과정에서 기존의 encoded 된 key 를 다시 encode 하고 있고 %
를 다시 인코딩하여 문제가 생긴다.
Decoded Key로 변경
그러면 encoding 이전의 key로 변경하고 전달하면 해결되지 않을까
final String url = "<https://apis.data.go.kr/6410000/busrouteservice/getBusRouteInfoItem>";
final String decodedKey = "QIMPxKy+/lt~~~~~==";
final var uri = UriComponentsBuilder.fromUriString(url)
.queryParam("serviceKey", decodedKey)
.queryParam("routeId", "234000031")
.toUriString();
// OpenAPI 호출
ResponseEntity<String> response = restTemplate.getForEntity(uri, String.class);
여전히 같은 오류를 받았으며, QIMPxKy+/lt~~~~~%3D%3D
라고 encode 된 것을 확인했다.
=
의 경우 정상적으로 encode 되어 %3D
로 인코딩되었으나, +
와 /
의 경우 인코딩되지 않은 것.
https://stackoverflow.com/questions/18138011/url-encoding-using-the-new-spring-uricomponentsbuilder
UriComponentBuilder
를 통해서는 정상적으로 Request 가 불가한 상황이다.
String 합치기
final String uri = url + "?serviceKey=" + encodedKey + "&routeId=" + routeId;
이번에는 String 에서 합치기만 했다. 그러나 여전히 Response 는 같다. (대체 왜?)
Browser 에서는 정상인 링크로 바로 보내기
final String uri = "<https://apis.data.go.kr/6410000/busrouteservice/getBusRouteInfoItem?serviceKey={KEY}&routeId=234000031>";
Browser 에서는 아무런 문제가 없는 링크로도 전송해보았다. 분명 성공해야하는데, 여전히 Response 는 같다.
답답해서 wireshark 로 까봤다.
wireshark 삽질
private relay 가 켜져있는지 꼭 확인하자. 모든 패킷이 private relay 를 통하기에 wireshark 에서 볼 때는 다 암호화가 되어 있다.
이 때문에 HTTP 패킷을 볼 수 가 없었다.
Wireshark 에서 확인해보니 또 다시 encoding 과정을 거친 값으로 실제 리퀘스트가 갔음을 확인했다.
Debugger 에서 URI 값을 확인했을 때는 정상적으로 parameter가 넘어갔다. 그러나, 결국 실제 request 로 날라갈 때는 또 인코딩이 된 상태로 넘어간다. 즉, parameter 상에서 URLEncoded 된 URI 를 넘겨도 또 다시 RestTemplate
의 요청 과정 어디에선가 Encoding 을 한다.
범인찾기 - RestTemplate
RestTemplate
의 과정을 살펴보면 결국 URL 대상으로 요청을 execute
하는 부분이 있다. 이 부분에서 URL 로 다시 바꿔서 요청을 실행하는데 이 부분이 문제다.
불행하게도 위에서와 같은 이유로 decoded 된 key 를 넘기더라도 +
와 같은 문자열이 인코딩되지 않아 정확한 key 를 보낼 수 없다.
따라서, 해당 과정을 피하는 방법을 떠올려야한다.
해결 방안
final URI uri = URI.create(uriString);
딱 이 한 줄만 추가하면 된다...
URIComponentBuilder
부분에서 encoding 하는 문제의 경우에도 해결할 수 있다. 다음과 같이 작성해보자.
final var builder = new DefaultUriBuilderFactory();
builder.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
final String uriString = builder.builder()
.scheme("http")
.host("apis.data.go.kr")
.path("/6410000/busrouteservice/getBusRouteInfoItem")
.queryParam("serviceKey", encodedKey)
.queryParam("routeId", routeId)
.build()
.toString();
final URI uri = URI.create(uriString);
사실 아래 글에서 처음에 찾긴 했는데, 어디서 문제를 발생시키는지 당최 파악이 안돼서 열심히 파봤다. 쓸 때마다 늘 염두에 둘 부분인 것 같아 글로 기록해둔다.