Interceptor 와 ArgumentResolver 를 사용한 인증 작업

알파로그·2024년 1월 29일
0

Spring Boot

목록 보기
54/57

✏️ 필요성

예를들어 인증, 인가와 같은 작업을 Controller 에서 하게되면 Controller 의 책임이 불필요하게 많아지고,
비즈니스로직과 섞여 유지보수가 어려워지는 문제점이 발생한다.

Intrerceptor 와 ArgumentResolver 를 사용해 요청이 API 에 라우팅 되기 전,
공통 처리 작업들을 사전에 하면 명확한 책임 분리와 많은 중복을 제거할 수 있게된다.

📍 예제

  • 로그아웃을 위해 Refresh Token 과 AccessToken 을 폐기하는 API 이다.
    • 클라이언트의 토큰들을 폐기 처리하기위해 인자로 받고있는데,
      토큰 조회, 유효성 검증하는 로직과 API 의 비즈니스 로직이 섞여 책임이 분리되지 않은 모습이다.
@GetMapping("/logout")
public ResponseEntity logout(
        @RequestHeader("Authorization") String accessToken,
        HttpServletRequest request
) {
    log.info("logout 요청 확인");

    String refreshToken = jwtService.getRefreshToken(request.getCookies());
    Member member = jwtService.getMember(accessToken);
    jwtService.expireToken(accessToken, refreshToken);

    log.info("logout 완료 / member id = {}", member.getId());
    return ResponseEntity.noContent()
            .build();
}

✏️ Interceptor 적용하기

  • Interceptor 를 사용해 API 에 요청이 라우팅되기 전에 요청을 가져와 전, 후 처리작업을 수행할 수 있다.

📍 Interceptor 객체

  • JWT 를 검증하는 로직을 만들어 인증이 필요한 모든 API 의 중복을 제거할 수도 있다.
import com.atowz.auth.infrastructure.jwt.JwtService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Slf4j
@Component
@RequiredArgsConstructor
public class AccessTokenInterceptor implements HandlerInterceptor {

    private final JwtService jwtService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String accessToken = request.getHeader("Authorization");

        if (accessToken != null) {
            jwtService.isAccessTokenValid(accessToken);
        }

        return true;
    }

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

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}

📍 Config 객체

  • 생성한 Interceptor 객체를 Interceptor registry 에 등록해준다.
    • addPathPatterns() 를 사용해 특정 uri 에 대한 요청만 반응하도록 할 수 있다.
    • 만약 다른 Interceptor 객체도 존재할경우 registry 에 동일한 방법으로 한번 더 등록해주면 된다.
      • 실행 우선순위는 먼저 등록된 interceptor 가 가져간다.
import com.atowz.auth.infrastructure.jwt.JwtService;
import com.atowz.global.interceptor.AccessTokenInterceptor;
import com.atowz.global.interceptor.RefreshTokenInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final JwtService jwtService;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AccessTokenInterceptor(jwtService))
                .addPathPatterns("/**");
    }
}

✏️ Handler method argument resolver

  • Interceptor 로 토큰의 유효성 검사를 통해 역할 분리와 중복제거를 했지만 여전히 API 에서 공통작업을 수행하고있다.
    • 토큰에 저장된 회원정보를 사용해 회원 객체를 조회하고,
      Controller 에 인자로 전달할 수 있다.

📍 어노테이션 생성

  • 어노테이션 방식으로 argument resolver 를 작동시키기 위해 생성 해줘야한다.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.Documented;

@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessTokenToMember {

    boolean required() default true;
}

📍 변환 작업 객체 생성

  • interceptor 에 의해 유효성 검증이 완료된 토큰을 회원 객체로 변환하고,
    Controller 의 인자로 전달하는 역할이다.
import com.atowz.auth.infrastructure.jwt.JwtService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
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
@RequiredArgsConstructor
public class AccessTokenArgumentResolver implements HandlerMethodArgumentResolver {

    private final JwtService jwtService;

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

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        String accessToken = request.getHeader("Authorization");
        return jwtService.getMember(accessToken);
    }
}

📍 Config 객체 생성

  • 아래와 같이 복수의 리졸버가 존재하면 여러번 등록해주면 된다.
import com.atowz.global.argumentResolver.accessTokenToMember.AccessTokenArgumentResolver;
import com.atowz.global.argumentResolver.getToken.TokenArgumentResolver;
import com.atowz.global.argumentResolver.refreshTokenToMember.RefreshTokenArgumentResolver;
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 WebConfig implements WebMvcConfigurer {

    private final AccessTokenArgumentResolver accessTokenArgumentResolver;
    private final RefreshTokenArgumentResolver refreshTokenArgumentResolver;
    private final TokenArgumentResolver tokenArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(accessTokenArgumentResolver);
        resolvers.add(refreshTokenArgumentResolver);
        resolvers.add(tokenArgumentResolver);
    }
}

✏️ 결과

  • 토큰의 유효성 검사와 회원 객체 조회 작업을 각각 분리해 반복되는 공통 작업을 최소화 하고,
    Controller 계층에서는 순수한 비즈니스로직의 흐름만 남게 되었다.
@GetMapping("/logout")
public ResponseEntity logout(
				@GetToken TokenRequest tokenRequest, 
				@AccessTokenToMember Member member) {
    log.info("logout 요청 확인");

    jwtService.expireToken(tokenRequest);

    log.info("logout 완료 / member id = {}", member.getId());
    return ResponseEntity.noContent()
            .build();
}
profile
잘못된 내용 PR 환영

0개의 댓글