이전글에서 언급한 Filter에 @RequestBody 데이터 가져오기에 대한 구현을 직접 해보겠습니다. 이전글을 참고해주세요.
spring 파라미터와 Request정보가 같이 필요할 때 처리방법
spring 파라미터와 Request정보가 같이 필요할 때 처리방법(1)
아래와 같이 Controller와 Dto가 있을때, 파라미터로 받은 restAPITestDto의 변수와, Request의 url이나 헤더와 같은 정보를 같이 로깅하는 상황이 실무에서 생겨, 이에 대해 고민했던 처리방법을 적어보고
dnl1029.tistory.com
Controller에서 @RequestBody안에는, 파라미터로 사용할 객체를 입력받아 Service단에서 사용하고 있습니다. 그리고 Restapi 요청에는 Http 요청의 Body문 뿐만아니라, Header에도 정보를 담아서 Http 요청을 보낼 수 있습니다. @RequestBody에 사용된 객체에 대한 정보와, Http 요청에 대한 정보(ex : 헤더정보) 가 같이 사용이 필요할 때, RequestContextHolder를 사용하면 Spring 전역으로 request정보를 가져올 수 있어 Controller나 Service에 구현하면 간단하게 구현할 수 있습니다. 그러나, 실무에서 컨트롤러나 서비스 외에Filter나 인터셉터에 적용할 일이 생겨, 구현해보았습니다. 저는 로깅과 apiKey 인증/인가를 적용하기 위해 필터에 해당 내용을 적용 했습니다.
필터나 인터셉터에서 @RequestBody에서 사용된 객체를 불러와서 읽으면, 필터나 인터셉터에서는 사용할 수 있으나, Controller까지 넘어가지 않습니다. 즉, 한번 사용되면 재사용되지 않는 성질이 있어, 필터나 인터셉터에서 @RequestBody 데이터를 읽으려면 Wrapper를 통해서 감싸줘야 합니다. Wrapper는 잘 작성된 글이 있어, 그대로 참고하였습니다.( ref : https://stuffdrawers.tistory.com/9)
- ReadableRequestBodyWrapperIOUtils의 toByteArray를 사용하려는데, import가 되지 않아, 아래와 같이 pom.xml에 의존성을 추가하였습니다.
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
.
package com.example.blog.util;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ReadableRequestBodyWrapper extends HttpServletRequestWrapper {
class ServletInputStreamImpl extends ServletInputStream {
private InputStream inputStream;
public ServletInputStreamImpl(final InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public boolean isFinished() {
//TODO Auto-generated method stub
return false;
}
@Override
public boolean isReady() {
//TODO Auto-generated method stub
return false;
}
@Override
public int read() throws IOException {
return this.inputStream.read();
}
@Override
public int read(final byte[] b) throws IOException {
return this.inputStream.read(b);
}
@Override
public void setReadListener(final ReadListener listener) {
//TODO Auto-generated method stub
}
}
private byte[] bytes;
private String requestBody;
public ReadableRequestBodyWrapper(final HttpServletRequest request) throws IOException {
super(request);
InputStream in = super.getInputStream();
//request의 InputStream의 content를 byte array로 가져오고 따로저장한다.
this.bytes = IOUtils.toByteArray(in);
this.requestBody = new String(this.bytes);
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bytes);
return new ServletInputStreamImpl(byteArrayInputStream);
}
public String getRequestBody() {
return this.requestBody;
}
}
이제 필터에서 requestbody 정보를 사용해, apikey를 통한 인증인가를 간단히 구현하겠습니다. 필터는 이전에 작성한 AuthorizationFilter를 재활용하였습니다. 필터에 대한 글은 이전 글을 참고해주세요.
spring Filter
본 글에서는 모든 Rest API에 특정작업을 Rest API 종류와 무관하게 모두 적용할 수 있는 Spring Filter에 대해 알아보겠습니다. Controller에서 몇개의 메서드가 있던, Http Method 방식이 Get이건 Post이건 무관
dnl1029.tistory.com
AuthorizationFilter를 수정하였습니다. 위에서 따로 만들었던 wrapper를 import하여 사용하였으며, wrapper의 setAttribute를 통해 설정을 해놓으면, 필터 뒤에 인터셉터에서도 해당 정보를 사용가능할 것입니다. @RequestBody를 통해 들어온 객체가, json 형식의 String으로 반환된게 wrapper.getRequestBody() 입니다. 이를 다시 객체로 변환시켜 원하는 값을 사용하기 위해서는 별도로 JsonUtil파일을 만들어, String json -> 객체로 변환시켜 줍니다. HttpServletRequest에서 getHeader를 조회한 값을 String apiKey에 저장하고, 객체에 있는 apiKeyDto.getApiKey()와 비교하여 인증/인가를 적용해주었습니다. 주의해야할 점은, try문안에 filterChain.doFilter를 wrapper로 고치지않고, 그대로 servletRequest로 할 경우, requestbody정보가 넘어가지 않습니다. 추가적으로, 실제로는 requestBody정보에 apiKey값을 직접넣지 않고, 사용자 ID같은것이 들어오면 DB에 저장된 사용자ID - apiKey 매핑 테이블을 통해 비교를 하는데, 아래 예제에서는 내용 단순화를 위해 DB에서 apiKey를 불러오는 부분을 생략하였습니다.
- Filter
@Slf4j
public class AuthorizationFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
// String apiKey = req.getHeader("apikey");
//
// try{
// if(!"testkey123".equals(apiKey)) {
// throw new RuntimeException("filter invalid api key");
// }
// //핵심 코드 doFilter
// filterChain.doFilter(servletRequest,servletResponse);
// }
// catch (Exception e){
// throw e;
// }
try {
//Filter에서 request body를 읽으면 controller로 못넘기기 때문에, wrapper로 감싸서 읽을수 있도록 구현
ReadableRequestBodyWrapper wrapper = new ReadableRequestBodyWrapper((HttpServletRequest) servletRequest);
wrapper.setAttribute("requestBody", wrapper.getRequestBody());
String requestBody = wrapper.getRequestBody();
log.info("requestBody : {}", requestBody);
//String json을 객체로 변환
ApiKeyDto apiKeyDto = JsonUtil.fromJson(requestBody, ApiKeyDto.class);
log.info("객체로 변환 후 값 : {}", apiKeyDto);
String apiKey = req.getHeader("apikey");
//실제로는 객체에서 ID값을 불러와, db에 저장된 것을 조회하여, request로부터 오는 apiKey와 일치하는지를 검사.
String testkey = apiKeyDto.getApiKey();
if(!testkey.equals(apiKey)) {
throw new RuntimeException("invalid api key");
}
filterChain.doFilter(wrapper, servletResponse);
}
catch (Exception e){
//try 문에 api 인증에 걸렸을때도, 다음 필터로 진행되길 원하면 catch안에 doFilter 구현
// filterChain.doFilter(servletRequest,servletResponse);
//try 문에 걸리면 멈추려면 아래와 같이 예외 throw
throw e;
}
finally {
log.info("AuthorizationFilter is applied");
}
}
}
json 형식의 String을 객체로 변환시키는 Util class 입니다.
- JsonUtil
@Slf4j
public class JsonUtil {
private JsonUtil() {
throw new IllegalStateException("JsonUtil class has static method only");
}
private static final ObjectMapper mapper = new ObjectMapper();
static {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
}
// object -> String json 리턴
public static String toJson(Object o){
String json = null;
try{
json = mapper.writeValueAsString(o);
}
catch (JsonProcessingException e) {
log.warn(e.getMessage());
}
return json;
}
// String json -> T object로 리턴
public static <T> T fromJson(String json, Class<T> type) {
if (json == null || type == null) {
return null;
}
T result = null;
try {
result = mapper.readValue(json, type);
}
catch (IOException e) {
log.warn(e.getMessage());
}
return result;
}
// String json -> T object로 리턴, param : TypeReference
public static <T> T fromJson(String json, TypeReference<T> type) {
if (json == null || type == null) {
return null;
}
T result = null;
try {
result = mapper.readValue(json, type);
}
catch (IOException e) {
log.warn(e.getMessage());
}
return result;
}
}
- ApiKeyDto
@Data
public class ApiKeyDto {
private String id;
private String name;
private String apiKey;
}
- RestApiController
@PostMapping("apikey/test")
public String apiKeyTest(@RequestBody ApiKeyDto apiKeyDto) {
log.info("RestAPIController. apiKeyDto : {}",apiKeyDto);
return "ok";
}
이제 구현은 끝났고, 테스트를 위해 Postman으로 restapi 요청을 날려보겠습니다. (http://localhost:8080/api/apikey/test)
RequestBody에는 아래와 같이 작성해줍니다.
헤더에도 동일하게 "testapikey123"이란 값을 넣고 Post 요청을 보내보겠습니다.
IntelliJ에 log가 정상적으로 찍힙니다. apikey인증인가가 성공했습니다.
이번엔 일부러 헤더에 다른 값을 넣어보겠습니다.
아래와 같이 RuntimeException을 뱉고, RestAPIController. ~~라는 로그가 없는 것으로 보아 Controller까지 Request가 넘어가지 않은 모습을 확인할 수 있습니다.
'spring' 카테고리의 다른 글
spring bean 초기화, 소멸(lifecycle) (0) | 2023.06.25 |
---|---|
spring jasypt를 활용한 DB접속정보 암호화 (0) | 2023.06.25 |
Spring Data JPA 쿼리메서드 방식으로 MongoDB 연계 (0) | 2023.06.04 |
spring RequestBodyAdviceAdapter를 통한 apiKey 인증인가 적용 (0) | 2023.06.04 |
spring 파라미터와 Request정보가 같이 필요할 때 처리방법 (1) | 2023.05.20 |
spring Interceptor (0) | 2023.05.20 |
spring Filter (0) | 2023.05.20 |
spring Bean Validation 적용(@NotNull, @NotEmpty, @NotBlank, @Max, @Min) (0) | 2023.05.19 |
댓글