Interceptor란
Spring이 제공하는 기술로써, 디스패처 서블릿(Dispatcher Servlet)이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다
Filter와 매우 유사한 형태로 존재하지만, 차이점은 Spring Context에 등록이 된다.
AOP와 유사한 기능을 제공할 수 있으며, 주로 인증단계를 처리하거나, Logging하는 데에 사용한다.
Interceptor 동작 위치 및 순서
1. DispatcherServlet은 Request 객체를 받아서 분석한뒤 HandlerMapping에게 사용자의 요청을 처리할 핸들러를 찾도록 요청 한다.
2. 그결과로 HandlerExecutionChain이 동작하게 되는데, 이 HandlerExecutionChain은 하나이상의 핸들러 인터셉터를 거쳐서 컨트롤러가 실행될수 있도록 구성되어 있다.
만약 핸들러 인터셉터를 등록하지 않았다면, 곧바로 컨트롤러가 실행되고, 반대로 하나이상의 인터셉터가 지정되어 있다면 지정된 순서에 따라서 인터셉터를 거쳐서 컨트롤러를 실행한다
3. HandlerExectuonChain이 각 Interceptor의 preHandle 메서드를 호출해서 요청을 가로채 요청에 대한 사전 작업을 수행한다.
4. preHandle 메서드를 실행 후, 적절한 컨트롤러를 호출하고, 컨트롤러는 요청을 처리하고 결과를 반환한다.
5. Interceptor의 postHandle 메서드가 실행되어 컨트롤러에서 반환된 결과를 가로채고, 후속 작업을 수행한다.
6. postHandle수행 후, HandlerMapping은 적절한 View를 렌더링 하여 클라이언트에게 응답을 보낸다.
7. 응답이 완료되면 Interceptor의 AfterCompletion메서드를 호출하여 리소스 정리나 로깅등의 작업을 수행한다.
Interceptor의 사용
Interceptor는 org.springframework.web.servlet에 HandlerInterceptor 인터페이스로 존재하고 아래의 세 메서드를 구현해야한다.
preHandle
preHandle 메서드는지정된 컨트롤러의 동작 이전에 가로채는 역할로 사용한다.
preHandle()의 경우 세 개의 파라미터를 사용하는데, HttpServletRequest, HttpServletResponse, Object handler로 구성된다.
마지막의 Handler는 현재 실행하려는 메소드 자체를 의미하는데,
이를 활용하면 현재 실행되는 컨트롤러를 파악하거나, 추가적인 메소드를 싱행하는 등의 작업이 가능하다.
postHandle
지정된 컨트롤러의 동작 이후에 처리하며, DispatcherServlet이 화면을 처리하기 전에 동작한다.
컨트롤러 실행이 끝나고 아직 화면처리는 안 된 상태이므로, 필요하다면 메소드의 실행 이후에 추가적인 작업이 가능하다.
afterCompletion
DispatcherSerlvet의 화면 처리(뷰)가 완료된 상태에서 처리한다.
위 인터셉터를 활용해서 아주아주 간단한 @Auth 어노테이션을 만들어서 해당 어노테이션으로
queryString에 token으로 들어온 값을 존재해야 연결이 가능한 Interceptor를 구현해 보겠다.
먼저 @Auth 어노테이션을 만든다
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}
그리고 인증절차를 거칠 Controller에 @Auth 어노테이션을 붙여준다,
@RequestMapping("/api/v3")
@RestController
@Auth
public class PrivateController {
@GetMapping("/hello")
public String hello(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return "private hello";
}
}
이제 HandlerInterceptor를 구한다.
우선 HandlerInterceptor를 상속 받고 Spring에서 관리할 수 있도록 @Component를 붙여준다.
queryString에 내가 기대한 token 값이 존재 하는지에대한 판단을 해야하므로 우선 uri와 queryString을 가져온다.
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 요청 uri
String reqUri = request.getRequestURI();
// queryString을 담은 URI 객체 생성
URI uri = UriComponentsBuilder.fromUriString(reqUri)
.query(request.getQueryString())
.build()
.toUri();
log.info("request uri: {}", uri);
return false;
}
}
그런 후 @Auth가 붙어있는지 판단하는 함수를 작성한다.
private boolean checkAnnotation(Object handler, Class clazz) {
// HandlerMethod가 아니라 정적 메서드로 왔다면 이는 인증처리할 필요 없음
if(handler instanceof ResourceHttpRequestHandler) {
return true;
}
// 핸들러 메서드로 형변환 후
HandlerMethod handlerMethod = (HandlerMethod) handler;
//noinspection unchecked
// 해당 어노테이션이 있거나 해당 타입의 빈이 존재하면 인증처리 해야함
return null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz);
}
위에 생성한 checkAnnotation함수를 사용해서 @Auth 어노테이션이 있으면 token이라는 값이 queryString으로 들어왔는지 확인 작업을 거친다.
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String reqUrl = request.getRequestURI();
URI uri = UriComponentsBuilder.fromUriString(reqUrl)
.query(request.getQueryString())
.build()
.toUri();
log.info("request uri: {}", uri);
// 인증처리를 해야하는지
boolean hasAnnotation = checkAnnotation(handler, Auth.class);
// 해야하면
if(hasAnnotation) {
// query를 가져와서
String query = uri.getQuery();
log.info("query: {}", query);
// null이거나 포함되어있지 않으면 false로 controller로 해당 요청이 가지 않음
// (query != null과 같은 falsy하고 뒤의 query.contains에서 null에러가 나지 않도록 먼저 처리
return query != null && query.contains("token=");
}
// 어노테이션 없으면 그냥 통과
return true;
}
private boolean checkAnnotation(Object handler, Class clazz) {
if(handler instanceof ResourceHttpRequestHandler) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
//noinspection unchecked
return null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz);
}
}
여기까지 하면 단순하게 @Auth가 붙은 컨트롤러에 대해서 queryString에 token을 확인하는 작업을 완료 하게 된다.
이제 이러한 로직을 작성하고 기능을 구현했으니 이게 실제 동작할 수 있도록 등록을 해주어야한다.
별다른 어려운 어려운 동작은 필요하지 않고 @Configuration에 등록해주면 된다.
@Configuration
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/v2/*", "/api/v3/*");
}
}
위와 같이 작성하면 지금까지 작성한 AuthInterceptor를 등록하고 해당 Interceptor를 적용시킬 urlPattern들 까지 설정을 마친 것이다.
'Back-End > Spring Boot' 카테고리의 다른 글
Spring Boot - RestTemplate를 사용한 Server To Server 연결(2) (0) | 2024.06.22 |
---|---|
Spring Boot - RestTemplate를 사용한 Server To Server 연결(1) (0) | 2024.06.18 |
Spring Boot Filter란 (2) | 2024.06.16 |
Spring Boot @RestControllerAdvice를 이용한 예외 처리 방법 (0) | 2024.06.09 |
AOP (0) | 2024.06.08 |