Part3 - CH6. 스프링 기능 활용하기(5) Interceptor
Interceptor
Interceptor란 Filter와 매우 유사한 형태이지만, Spring Context에 등록된다는 차이점이 있다.
AOP와 유사한 기능을 제공할 수 있으며, 주로 인증단계
를 처리하거나 Logging하는데에 사용한다.
Life-Cycle 도식도를 보면 알겠지만, Filter와는 다르게 DispatcherServlet을 거친 후에 Controller 와 같은 영역에 있다.
Interceptor 활용하기
권한이 있는 곳에서의 요청만 받는 컨트롤러와, 어떤 요청이라도 받는 컨트롤러를 만든다고 가정하고 실습을 진행 해보자.
@RestController
@RequestMapping("/api/public")
public class PublicController {
@GetMapping("/hello")
public String hello(){
return "hello public";
}
}
@RestController
@RequestMapping("/api/private")
public class PrivateController {
@GetMapping("/hello")
public String hello(){
return "hello private";
}
}
이렇게 컨트롤러를 먼저 만들어 준 후에 이제 권한이 있는지 확인을 해야한다. 이는 다음과 같이한다.
권한이 있는지 없는지 검사하는 기준을 만들어주기 위해 어노테이션을 붙여놓고 해당 어노테이션이 있으면 세션에 권한이 있는지 여부를 검사하도록 하겠다.
@Auth라는 어노테이션을 만들어서 PrivateController에만 붙여보도록 하자.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}
@RestController
@RequestMapping("/api/private")
@Auth
public class PrivateController {
@GetMapping("/hello")
public String hello(){
return "hello private";
}
}
이제 인터셉터를 만들어보자. HandlerInterceptor를 상속받아서 preHandler를 오버라이드 하자.
그리고, 해당요청을 받는 컨트롤러에 Auth어노테이션이 붙었는지 확인하는 메소드도 하나 넣어준다.
인터셉터를 위한 @Auth 어노테이션은 메소드에도 붙일수는 있지만 그렇게 사용하는 경우는 별로 없다.
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI();
// Filter때와 동일하게 body를 읽으면 손실되므로 ContentCachingRequestWrapper를 사용하여 읽어야한다.
// 그런데 형변환은 Filter단에서만 가능하므로 Filter에서 미리 형변환을 해서 넘겨주어야만 가능하다.
URI uri = UriComponentsBuilder.fromUriString(request.getRequestURI())
.query(request.getQueryString()).build().toUri();
log.info("request url : {}", url);
//어노테이션 체크
boolean hasAnnotation = checkAnnotation(handler, Auth.class);
log.info("has annotation : {}", hasAnnotation);
// 나의 서버는 모두 public으로 동작. 단, Auth 권한을 가진 요청에 대해서 보통 세션, 쿠키, 등을 검사
if(hasAnnotation){
//권한 체크
String query = uri.getQuery();
if(query.equals("name=dboo")){
return true;
}
return false;
}
return true;
}
private boolean checkAnnotation(Object handler, Class clazz){
// resource javascript, html, ... 일때는 통과
if( handler instanceof ResourceHttpRequestHandler){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
if(null !=handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)){
// Auth annotation 이 있다면 true 반환
return true;
}
return false;
}
}
마지막으로, 만들어준 인터셉터를 스프링의 인터셉터에 등록해준다.
@Configuration
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {
// @Autowired -> 순환참조를 막기위해서 final선언 후 @RequiredArgsContructor를 많이 쓰는 추세이다.
private final AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor);
// .addPathPatterns("/api/private/*"); //특정 pathPattern에 대해서만 인터셉트 하도록 할 수 있다.
}
}
Interceptor 에서의 예외 처리하기
지난 시간에 배웠던 전역적 예외처리하는 방법을 적용하여, 인터셉터에서 throw 시킨 에러를 잡아서 처리하는 핸들러를 만들어서 적용시켜보자.
일단, 커스텀 Exception타입을 만든다. 이름은 AuthException으로 하자.
public class AuthException extends RuntimeException{
}
그리고 인터셉터에서 쿼리파라미터가 예상과 다를 시, AuthException을 throw하도록 바꾼다.
// 나의 서버는 모두 public으로 동작. 단, Auth 권한을 가진 요청에 대해서 보통 세션, 쿠키, 등을 검사
if(hasAnnotation){
//권한 체크
String query = uri.getQuery();
if(query.equals("name=dboo")){
return true;
}
//return false;
throw new AuthException();
}
return true;
마지막으로 던져진 AuthException을 받아서 처리할 핸들러를 만든다.
예외 상태는 UNAUTHORIZED로 하자.
@RestControllerAdvice
public class GlobalExcptionHandler {
@ExceptionHandler(AuthException.class)
public ResponseEntity authExeption(){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
필터는 어노테이션을 통한 제어를 할 수 없다.