처음 코드에는 컨트롤러에 JWT 유효성 검사하는 코드가 있었는데, 이는 매우 비효율적이며, 반복적인 코드를 계속 작성하게 한다.
이때 인터셉터라는 개념을 처음 익혔다.
인터셉터란?
인터셉터(Interceptor)는 J2EE 표준 스펙인 필터(Filter)와 달리 스프링이 제공하는 기술로서, 디스패처 서블릿이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다. 즉, 웹 컨테이너에서 동작하는 필터와 달리 인터셉터는 스프링 콘텍스트에서 동작을 하는 것이다.
디스패처 서블릿은 핸들러 매핑을 통해 적절한 컨트롤러를 찾도록 요청하는데, 그 결과로 실행 체인(HandlerExecutionChain)을 돌려준다. 그래서 이 실행 체인은 1개 이상의 인터셉터가 등록되어 있다면, 순차적으로 인터셉터를 거쳐 컨트롤러가 실행되도록 하고, 인터셉터가 없다면 바로 컨트롤러를 실행한다.
인터셉터 구현
인터셉터를 추가하기 위해서는 HandlerInterceptor 인터페이스를 구현해야 하며, 이는 다음의 3가지 메소드를 가지고 있다.
- preHandle 메소드
- preHandle 메서드는 컨트롤러가 호출되기 전에 실행된다. 그렇기 때문에 컨트롤러 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공하거나 추가하는 경우에 사용할 수 있다.
- preHandle 메서드의 3번째 파라미터인 handler 파라미터는 @RequestMapping이 붙은 메소드의 정보를 추상화한 객체이다.
- preHandle 메소드의 반환 타입은 boolean인데, 반환값이 true이면 다음 단계로 진행이 되지만, false라면 작업을 중단하여 이후의 작업(다음 인터셉터 또는 컨트롤러)은 진행되지 않는다.
- postHandle 메소드
- postHandle 메서드는 컨트롤러를 호출된 후에 실행된다. 그렇기 때문에 컨트롤러 이후에 처리해야 하는 후처리 작업이 있을 때 사용할 수 있다.
- afterCompletion 메소드
- afterCompletion 메소드는 이름 그대로 모든 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 실행된다.
인터셉터를 추가해 보자
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private final JwtProvider jwtProvider;
public boolean preHandle(final HttpServletRequest request, HttpServletResponse response, Object handler) {
if (CorsUtils.isPreFlightRequest(request)) {
return true;
}
final String token = AuthorizationExtractor.extract(request);
if (!jwtProvider.isValidToken(token)) {
//TODO: 커스텀 exception으로 변경
throw new IllegalArgumentException();
}
return true;
}
}
JWT 유효성 검사는 요청이 온 후 처리가 되어야 하므로 preHandle 메서드를 사용하려고 한다.
컨트롤러에 있던 유효성 검사 코드를 preHandle 메서드로 옮긴다.
이제 클라이언트가 요청이 올 때 바로 인터셉터가 가로채서 토큰에 유효성 검사를 하게 된다.
하지만, 토큰에 유효성 검사만 한 후 로그인 유저 객체로 변환하는 과정을 모든 컨트롤러마다 구현해야 한다. 그러면 사용자 검증이 필요한 컨트롤러에 중복 코드가 생기고, 컨트롤러의 책임이 증가하게 된다. 이러한 문제를 Argument Resolver가 해결해 줄 수 있다.
Argument Resolver
Argument Resolver는 어떠한 요청이 컨트롤러에 들어왔을 때, 요청에 들어온 값으로부터 원하는 객체를 만들어 내는 일을 간접적으로 해줄 수 있다.
Argument Resolver 구현
- supportsParameter 메서드
- ArgumentResolver가 실행되길 원하는 Parameter의 앞에 특정 어노테이션을 생성해 붙인다. supportsParameter() 메서드는 요청받은 메소드의 인자에 원하는 어노테이션이 붙어있는지 확인하고 원하는 어노테이션을 포함하고 있으면 true를 반환한다.
- resolveArgument 메서드
- supportsParameter에서 true를 받은 경우, 즉, 특정 어노테이션이 붙어있는 어느 메서드가 있는 경우 parameter가 원하는 형태로 정보를 바인딩하여 반환하는 메서드이다.
@Component
@RequiredArgsConstructor
@Slf4j
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
private final JwtProvider jwtProvider;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(AuthenticationPrincipal.class) != null
&& parameter.getParameterType().equals(Long.class);
}
@Override
public Object resolveArgument(
MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
final HttpServletRequest httpServletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
final String token = AuthorizationExtractor.extract(httpServletRequest);
return jwtProvider.getPayload(token);
}
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticationPrincipal {
}
AuthenticationPrincipal 정의하였다.
@AuthenticationPrincipal을 붙이면 jwtProvider.getPayload(token) 값으로 바인딩되어 로그인한 유저의 id를 사용할 수 있게 되었다.
'WEB' 카테고리의 다른 글
전략 패턴을 사용해 로그인 추상화 하기 (0) | 2024.08.08 |
---|---|
@NoArgsConstructor 액세스 레벨을 PROTECTED로 하는 이유 (1) | 2024.06.09 |
Redis 내부동작 파헤치기 (0) | 2024.05.05 |
Redis Lock 동시성 해결하기 (0) | 2024.05.01 |
JWT 활용기 (0) | 2024.04.12 |
Oauth를 사용해 카카오 로그인 구현 (0) | 2024.04.12 |
리사이징 적용기 with Marvin (0) | 2024.03.14 |
Spring Cloud Config 도입기 (0) | 2024.03.08 |