백엔드 개발일지

[JPA] 인터셉터를 활용한 JWT Token 기반 로그인 구현 정리 (feat. AOP를 이용한 사용자 토큰 추출)

jisu0708 2023. 7. 17. 14:46

1. 인터셉터

1) 인터셉터란?

인터셉터(Interceptor)는 스프링 프레임워크에서 요청과 응답을 가로채서 처리하는 기능입니다. 컨트롤러가 호출되기 전에 요청을 가로채서 다양한 처리를 할 수 있습니다.

  • ex) 로그인 체크, 권한 체크, 인코딩 설정, 로깅 등등...
  • 이렇게 핸들러(컨트롤러)의 수정없이 핸들러 수행 전/후처리 동작을 추가해 중복되는 코드를 줄이고 코드의 유지보수성 또한 높일 수 있다

 

2) HandlerInterceptor 인터페이스

인터셉터는 HandlerInterceptor 인터페이스를 구현해 작성해야 합니다.
HandlerInterceptor 인터페이스에는 다음 3가지 메서드가 정의됩니다.

public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }
}
  • preHandle() : preHandle() 메서드는 컨트롤러가 요청을 처리하기 전 호출된다.
    • 이 메서드에서 요청을 가로채 로그인 체크나 권한 체크 등의 처리를 할 수 있다.
    • 여기서 true를 반환해야 요청을 처리할 컨트롤러로 이동한다.
  • postHandle() : postHandle() 메서드는 컨트롤러가 요청을 처리한 후에 호출된다.
  • afterCompletion() : afterCompletion() 메서드는 요청 처리가 완료된 후에 호출된다.
    • 이 메서드에서는 예외 처리나 리소스 해제 등의 작업을 수행할 수 있다.

 

중요
인터셉터를 등록하려면 WebMvcConfigurer 인터페이스를 구현해야 한다. 그리고 addInterceptors() 메서드를 오버라이드 하여 인터셉터를 등록한다.

 

3) 동작 순서

고객 요청이 오면 WAS 거치고 필터 거치고 디스패처 서블릿으로 들어옵니다.
그러면 preHandle 을 호출하고 핸들러 어댑터를 통해 핸들러 호출을 해 ModelAndView 를 반환합니다.
다음으로 postHandle을 호출해 받았던 ModelAndView 를 넘겨준다. 다음 render 를 호출해 뷰를 렌더링 하고 afterCompletion 호출합니다.

 

정리

1. HandlerMapping 할당
2. HandlerAdapter 할당
3. preHandler
4. HandlerAdapter 수행
5. postHandler
6. View 렌더링
7. afterCompletion

 

 

 

2. 인터셉터 로그인 구현

1) 로그인 구현 설정

① 인터셉터 클래스 구현

  • 인터셉터를 구현하기 위해서는 먼저 HandlerInterceptor 인터페이스를 구현한 클래스를 작성해야 한다.
  • 이 클래스 안에서 preHandle, postHandle, afterCompletion 메서드를 구현하여 요청 전, 후, 완료 후에 수행할 로직을 작성한다.

② 인터셉터 등록

  • 인터셉터를 등록하기 위해 WebMvcConfigurer 인터페이스를 구현한 클래스를 작성한다.
  • 해당 클래스에서 addInterceptors 메서드를 오버라이드하여 인터셉터를 등록한다.

③ 인터셉터 적용

  • WebMvcConfigurer 인터페이스를 구현하는 클래스에서 addInterceptors 메서드를 오버라이드하여 인터셉터를 등록한다.
  • 또한 addPathPatterns 메서드를 이용하여 적용할 URL 패턴을 지정할 수 있다.
    • .addPathPatterns("/**") : 모든 URL에 인터셉터가 적용되도록 지정

④ 로그인 처리 구현

  • 로그인 컨트롤러에서 로그인 정보를 받아와 서비스 클래스를 호출하여 로그인 처리를 수행한다.

 

2) 동작 단계

JWT 기반 Interceptor 로그인 구현

AuthService 에서 login() 메서드를 통해 로그인이 이루어짐

  • 입력된 사용자 이메일을 통해 회원 정보를 데이터베이스에서 가져온 후, validatePassword 메서드를 사용하여 비밀번호가 일치하는지 확인
  • 비밀번호가 일치하면 issueToken 메서드를 사용하여 JWT 토큰을 발급하고, 해당 토큰을 TokenResponse 객체에 담아 반환

② JWT 토큰 발급과 검증은 JwtUtils 클래스에서 구현

  • createToken 메서드를 사용하여 JWT 토큰을 생성
  • validateToken 메서드를 사용하여 JWT 토큰이 유효한지 검증

③ JWT 토큰이 필요한 API 요청에 대해 LoginInterceptor 클래스에서 JWT 토큰을 검증하고, 유효하지 않을 경우 에러 응답을 보내주도록 구현

 

 

 

3. ArgumentResolver를 이용한 Authorization

일반적으로 대부분의 기능에는 로그인이 필수로 요구됩니다. 그렇기에 대부분의 API로직에는 토큰 검증을 하는 로직 코드가 추가적으로 들어가야하는데 이렇게 되면 같은 코드가 반복적으로 들어가야 하며 유지보수에도 좋지 않습니다.

 

이런 문제를 해결하기 위해 ArgumentResolver를 이용해 커스텀 어노테이션이 있다면 해당 어노테이션이 있는 매개변수에 대해 resolve해서 로그인 검증을 쉽게 할 수 있습니다.

 

 

1) AuthenticationPrincipalArgumentResolver

@Component
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {

    private final JwtTokenProvider jwtTokenProvider;

    public AuthenticationPrincipalArgumentResolver(final JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(LoginUserId.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        String token = AuthorizationExtractor.extractAccessToken(request);
        String payload = jwtTokenProvider.getPayload(token);
        return Long.parseLong(payload);
    }
}
@Hidden
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUserId {
}

 

위의 코드에서는 HandlerMethodArgumentResolver를 구현하여 사용자의 로그인 ID를 처리합니다.

supportsParameter 메서드는 해당 Resolver가 특정 파라미터를 지원하는지 여부를 결정하고, resolveArgument 메서드에서는 실제로 파라미터를 해결하는 로직이 구현되어 있습니다.

 

해당 Resolver는 @LoginUserId 어노테이션이 파라미터에 존재하는 경우에만 동작하도록 되어 있습니다. 또한, JWT 토큰을 통해 사용자의 인증 정보를 추출하고, 해당 정보에서 로그인 ID를 파싱하여 반환합니다.

 

 

 

2) AuthenticationPrincipalArgumentResolver

@Configuration
@RequiredArgsConstructor
public class AuthConfig implements WebMvcConfigurer {

    private final LoginInterceptor loginInterceptor;
    private final AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                //...
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(authenticationPrincipalArgumentResolver); // 추가
        WebMvcConfigurer.super.addArgumentResolvers(resolvers);
    }
}

위의 코드에서 addArgumentResolvers 메서드는 HandlerMethodArgumentResolver를 구현한 resolver들을 등록하는 역할을 합니다.

여기서는 authenticationPrincipalArgumentResolver를 리스트에 추가하고 있는데 Spring MVC는 해당 Resolver를 사용하여 컨트롤러 메서드의 인자를 해석하게 됩니다.

이렇게 등록된 Argument Resolver는 컨트롤러 메서드의 파라미터에 @LoginUserId 어노테이션이 있는 경우에 동작하며, AuthenticationPrincipalArgumentResolver 클래스에서 정의한 로직에 따라 사용자의 로그인 ID를 처리합니다. 이를 통해 컨트롤러 메서드에서 편리하게 로그인 ID에 접근할 수 있게 됩니다.

 

 

 

 

3. Interceptor 로그인 - 트러블슈팅 💫

1) WebMvcConfigurer 인터페이스를 구현한 클래스 누락

  • WebMvcConfigurer : Spring MVC에서 웹 애플리케이션의 구성을 설정할 수 있는 인터페이스

모카콩에서 로그인 기능을 구현하기 위해 Interceptor 를 도입했습니다.

기본적으로 InterceptorWebMvcConfigurer 에서 제공을 하는 기능인데 WebMvcConfigurer 인터페이스를 구현한 클래스를 누락하니 Interceptor 자체를 못쓰게 되면서 인터셉터 없는 인터셉터 로그인 기능을 만들었습니다…🥲

 

2) WebMvcConfigurer 구현 클래스에 Resolver 등록 누락

  • ArgumentResolvers() : WebMvcConfigurer 인터페이스의 추상 메서드 중 하나로, 컨트롤러 메서드의 매개변수를 해석하는 방법을 정의하는 역할을 한다

 

사용자 인증을 위해서는 JwtTokenProvider 를 이용하여 JWT 토큰으로부터 인증 정보를 추출해야 합니다.

이러한 역할을 하는 AuthenticationPrincipalArgumentResolver 객체를 resolver 에 등록해야 하는데 등록을 하지 않았으므로 컨트롤러 메서드에서 JWT 토큰으로부터 인증 정보를 추출하지 못하면서 회원을 찾지 못하는 에러가 났습니다.