스프링 인터셉터는, 서블릿 필터와 같이 컨트롤러 이전에 필터링을 하는 기능이다.
서블릿 필터가 HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 흐름이라면, 스프링 인터셉터는 HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 흐름이다.
필터와 서블릿은 둘다 체이닝을 제공하는데, 스프링 인터셉터가 훨씬 편의 기능을 많이 제공하고, pathPattern 추가가 용이하다.
서블릿 필터 적용 회고에서 작성했지만, 서블릿 필터에서는 urlPattern이 애매해서, 간단히 로그인 처리만 했었다. 하지만, 뭔가 흐름이 이상하지만 우선 배운다는 의미로 작성했었다. fetch로 html redirect가 들어오는 처리로 애먹었다.
스프링 인터셉터에서는 urlPattern 처리를 세분화 하고, Ssr 인터셉터, Rest 인터셉터로 나누어 등록했다. Ssr 인터셉터에서는 권한없음 페이지로 리다이렉트 시켜주지만, Rest 인터셉트에서는 권한 없음을 ResponseEntity<ErrorResponse>로 보내준다. 이전에 직접 만든 Response 객체 -> ResponseEntity<>로 변경에서 ResponseEntity는 다룬 적이 있다.
preHandle, postHandle, afterCompletion
인터셉터를 만들려면 org.springframework.web.servlet.HandlerInterceptor 인터페이스를 구현해야 한다. preHandle, postHandle, afterCompletion 세 가지 메서드를 제공하는데, 각각 컨트롤러 이전, 이후, DispatcherServlet이 view를 render를 한 이후 처리한다.
나는 로그인 인증, 어드민 인증에 사용할 인터셉트들을 만들어서 preHandle만 구현했다.
WebMvcConfigurer를 구현한 Configuration에 등록
인터셉터는 WebMvcConfigurer 인터페이스를 구현한 Configuration에 등록할 수 있는데, 포함할 pathPattern과 제외할 pathPattern을 넣어줄 수 있어서 매우 간편하다. 정규표현식까지 지원하므로, 중요한 순서대로 인터셉터를 등록했다.
ssrLogin -> restLogin -> ssrAdmin -> restAdmin
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final AdminService adminService;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SsrLoginCheckInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/join", "/signout", "/css/*", "/js/*", "/access-denied", "/*.ico", "/error")
.excludePathPatterns("/books/**", "/users/**");
registry.addInterceptor(new RestLoginCheckInterceptor())
.order(2)
.addPathPatterns("/books/**");
registry.addInterceptor(new SsrAdminCheckInterceptor(adminService))
.order(3)
.addPathPatterns("/admin/**");
registry.addInterceptor(new RestAdminCheckInterceptor(adminService))
.order(4)
.addPathPatterns("/books/{id:\\d+}", "/books/add");
}
}
Ssr 인터셉터
SsrLoginCheckInterceptor, SsrAdminCheckInterceptor는 모두 Ssr 요청에 해당하는 인터셉터다.
public class SsrLoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
response.sendRedirect("/access-denied?redirectUrl=" + requestURI);
return false;
}
return true;
}
}
@RequiredArgsConstructor
public class SsrAdminCheckInterceptor implements HandlerInterceptor {
private final AdminService adminService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
User user = (User) session.getAttribute("user");
String requestURI = request.getRequestURI();
if (!adminService.isAdmin(user.getId())) {
session.invalidate();
response.sendRedirect("/access-denied?redirectUrl=" + requestURI);
return false;
}
return true;
}
}
둘다, response에서 직접 redirect 처리를 해준다. 단, admin 체크시에는 세션을 초기화해주는 로직을 추가했다.
Rest 인터셉터
RestLoginCheckInterceptor, RestAdminCheckInterceptor는 모두 Rest 요청에 해당하는 인터셉터다.
public class RestLoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ObjectMapper mapper = new ObjectMapper();
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
throw new UnauthorizedAccessException("로그인해주세요.");
}
return true;
}
}
@RequiredArgsConstructor
public class RestAdminCheckInterceptor implements HandlerInterceptor {
private final AdminService adminService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (request.getMethod().equals("GET")) {
return true;
}
HttpSession session = request.getSession(false);
User user = (User) session.getAttribute("user");
if (!adminService.isAdmin(user.getId())) {
session.invalidate();
throw new UnauthorizedAccessException("관리자가 아닙니다.");
}
return true;
}
}
이 경우에 직접 객체를 만들어서 response에서 writer를 찾아 보내줄 수도 있겠지만, ObjectMapper를 다뤄야 되고, response 헤더도 넣어줘야 되는 귀찮음이 있어서, 예외를 던지고 @ControllerAdvice로 처리해주기로 했다.
public class UnauthorizedAccessException extends RuntimeException {
public UnauthorizedAccessException(String message) {
super(message);
}
}
@ControllerAdvice
public class InterceptorExceptionHandler {
@ExceptionHandler(UnauthorizedAccessException.class)
public ResponseEntity<ErrorResponse> handleUnauthorizedAccessException(UnauthorizedAccessException e) {
ErrorResponse errorResponse = ErrorResponse.builder()
.message(e.getMessage())
.build();
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
}
}
이렇게 하면, ResponseEntity를 만들어서 보내줄 수 있다. 이렇게 하면, 클라이언트쪽 fetch 함수에서는 기대하는대로 json만 받을 수 있다.