기존에 작성했었던 인증관련 로직을 사용했을때 중복되는 부분이 많았다. 대표적으로 컨트롤러에서 토큰값으로 사용자의 식별자 값을 추출하는 부분이다. 이 부분은 항상 고민이 많았다. 사실 2줄정도 추가만하면 되지만 뭔가 이런 Copy & Paste의 반복은 계속 보고 싶지 않다보니 해당 관련 공부를 시작하게 되었다.
private final AuthService authService; // DI
public ResponseEntity<?> createPost(@RequestBody CreatePostRequest createPostRequest){
Long memberId = authService.extractMemberIdFromToken();
CreatePostResponse response = postService.createPost(memberId, createPostRequest);
return ResponseEntity.ok(response);
}
HandlerMethodArgumentResolver 라는 인터페이스를 상속받은 클래스로 사용하면 특정 타입의 파라미터에 대한 처리를 할수 있다. 그래서 컨트롤러에 있는 메소드의 매개인자를 설정해줄수 있다.
supportsParameter(MethodParameter parameter): Resolver가 어떤 파라미터를 지원하는지 판단한다. 반환값이 true일때만 어노테이션을 지정한 변수 값을 바인딩해준다.
resolveArgument: 실제로 파라미터값을 지정해주는 로직이다. 해당 반환값이 지정한 파라미터 값이 된다.
import lombok.*;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
// 토큰을 추출하는 함수
private Long extractMemberIdFromToken() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
System.out.println(authentication);
if (authentication == null || authentication.getName() == null) {
throw new RuntimeException("토큰정보가 유효하지 않습니다.");
}
return Long.parseLong(authentication.getName());
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(CurrentUser.class) != null
&& parameter.getParameterType().equals(Long.class);
}
@Override
public Object resolveArgument(
MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
return extractMemberIdFromToken();
}
}
resolver를 사용하기 위해서 WebMvcConfigurer의 addArgumentResolvers를 상속받아 정의햬던 CurrentUserArgumentResolver를 리스트에 추가해줘야한다.
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final CurrentUserArgumentResolver currentUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(currentUserArgumentResolver);
}
}
현재 SecurityContextHolder에 저장되어있는 인증객체를 전달해야 하는데, CurrentUser라는 커스텀 어노테이션을 정의한다.
package example.ganada.common.annotaion;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
조치결과 컨트롤러에 직접 작성하지 않더라도 이번에 정의했던 코드를 통해 어노테이션만 지정하면 같은 기능을 할 수 있다. 또 PostController는 이제 AuthService를 의존할 필요가 없어졌다.
그리고 인증관련에 있어서 변경사항이 있으면 한 곳 만 바꿔주면 이 기능을 사용하는 모든 컨트롤러를 수정할 필요가 없어졌다.
@PostMapping
public ResponseEntity<?> createPost(@RequestBody CreatePostRequest createPostRequest, @CurrentUser Long memberId){
CreatePostResponse response = postService.createPost(memberId, createPostRequest);
return ResponseEntity.ok(response);
}