1. 과제
JWT 토큰을 사용하여 인증, 인가를 구현하라
2. 배운점
2.1. JWT
JWT 토큰을 적용하여 인증 처리를 하였다. 토큰 적용 후 사용목적이나 한계점, 효과적인 토큰 사용 방법들에 대해 공부하며 JWT 토큰에 대한 개념을 확립할 수 있었다. 정리한 개념은 따로 포스팅하였다.
https://tlatmsrud.tistory.com/87
2.2. Interceptor에 대한 테스트 코드
요청에 대한 권한 여부를 체크하는 방식으로 커스텀 어노테이션 방식을 채택했다.
클라이언트로부터 요청이 들어왔을 때 실행되는 컨트롤러 메서드에 커스텀 어노테이션(@CheckJwtToken)이 있을 경우 JWT 토큰을 검증하도록 하였으며 이를 위해 앞단에 Interceptor를 구현하였다.
Interceptor의 테스트 코드 작성 시 preHandler메서드에 대한 테스트 작성이 어려웠다. HttpServletRequest, HttpServletResponse, Handler에 대한 파라미터를 테스트코드에서 어떻게 넘겨야할지 감이 안잡혔기 때문이다. 우여곡절 끝에 MockHttpServletRequest, MockHttpServletResponse, HandlerMethod로 구현하게 되었다.(멘토님 감사합니다.)
HandlerMethod로 구현한 이유
디스패처 서블릿은 애플리케이션이 실행될 때 모든 컨트롤러의 메소드를 추출한 뒤 HandlerMethod 형태로 저장해두고, 실제 요청이 들어오면 요청 조건에 맞는 HandlerMethod를 참조하여 해당 메서드를 실행시킨다.
실제 API에 대한 요청이 들어올 경우 Interceptor의 preHandler 메서드 파라미터인 handler에 HandlerMethod가 들어오기 때문에 HandlerMethod로 구현하였다.
HandlerMethod 생성자 파라미터는 beanObject, methodName, parameter로 구성되는데, 테스트 클래스에 MockHandler 클래스를 하나 만들어 아래과 같이 구현하고, beanObject는 테스트 클래스를, methodName에는 @CheckJwtToken가 포함된 메서드 명을 기재하였다. 이로써 Interceptor에 대한 테스트코드를 작성할 수 있었다. 아래는 MockHandler 클래스와 preHandler 메서드에 대한 테스트 코드이다.
class MockHandler{
@CheckJwtToken
public void handleRequest(){
}
}
@Test
void withInvalidTokenReturnFalse() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HandlerMethod handler = new HandlerMethod(new MockHandler(), "handleRequest");
request.setMethod("GET");
request.addHeader("Authorization", "Bearer "+INVALID_TOKEN);
// 유효하지 않은 토큰일 경우 PreHandle 메서드에서 예외가 발생하며 최종적으로 false를 리턴함.
boolean result = loginCheckInterceptor.preHandle(request, response, handler);
assertThat(result).isFalse();
}
2.3. ParameterizedTest
테스트 메서드의 로직은 동일하나 특정 변수 값만 다른 테스트 케이스가 있다. 예를들어 유효하지 않은 토큰 검증에 대한 테스트 시 토큰의 요청 값으로 null, 빈 값, 쓰레기값이 들어갈 수 있는데, 이를 각각 테스트하기 위해 다음과 같이 3개 메서드로 구현할 수 있다.
void parseTokenWithBlankToken(){
assertThatThrownBy(() -> authenticationService.parseToken(" "))
.isInstanceOf(InvalidTokenException.class);
}
void parseTokenWithNullToken(){
assertThatThrownBy(() -> authenticationService.parseToken(null))
.isInstanceOf(InvalidTokenException.class);
}
void parseTokenWithInvalidToken(){
// INVALID_TOKEN 은 실제 유효하지 않은 토큰을 담은 static 변수임.
assertThatThrownBy(() -> authenticationService.parseToken(INVALID_TOKEN))
.isInstanceOf(InvalidTokenException.class);
}
위의 경우 테스트 로직은 동일하나 토큰 값이 달라서 각각의 메서드를 만들어 처리하였는데, 이처럼 특정 변수 값만 다르고 로직이 동일할 경우 ParameterizedTest를 통해 하나의 메서드로 테스트 가능하다.
@ParameterizedTest
@ValueSource(strings = { INVALID_TOKEN, "", null})
void parseTokenWithInvalidTokens(String token){
assertThatThrownBy(() -> authenticationService.parseToken(token))
.isInstanceOf(InvalidTokenException.class);
}
3. 느낀점
실무에서 JWT 토큰 적용을 검토했던 적이 있다. 적용을 목적으로 했던거라 개념적인 내용을 많이 건너뛰었던 것 같았는데 이번 기회에 JWT 토큰에 대한 기본 개념, 사용 이유, 보안상 이점, 한계점과 같은 것들을 공부하게 되어 좋았다.
테스트 코드는 Controller, Service, Repository 정도로만 구현을 하다가 이번에 처음으로 Interceptor를 구현하게 되었는데 처음 해보는거라 감이 아예 잡히지 않았었다. prehandle 메서드 검증을 위해 파라미터를 채우는 방법을 몰랐기 때문이었는데, 멘토님께서 도와주시어 해결할 수 있었다.
이제 2주 남았다. 남은 2주도 미루지 않고 잘 해보자!