Spring ‐ 필터와 인터셉터 - dnwls16071/Backend_Study_TIL GitHub Wiki
HTTP 요청 → 내장 톰켓 서버(WAS) → 필터 → 디스패처 서블릿 → 인터셉터 → 컨트롤러
- 필터를 적용하면 필터가 호출된 다음 서블릿이 호출되기 때문에 서블릿에 도달하기 전에 부가적인 작업을 먼저 처리할 수 있다.
- 스프링 하위 프레임워크인 스프링 시큐리티 역시 필터 체인으로 구성이 되어 있다.
- init
- 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
- doFilter
- 요청이 들어올 때마다 호출, 필터 로직을 구현하는 부분
- destroy
- 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
log.info("인증 체크 필터 시작 {}", requestURI);
if (isLoginCheckPath(requestURI)) {
log.info("인증 체크 로직 실행 {}", requestURI);
HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청 {}", requestURI);
//로그인으로 redirect
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return;
}
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 체크 필터 종료 {} ", requestURI);
}
}
/**
* 화이트 리스트의 경우 인증 체크X
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
- ServletRequest, ServletResponse
- 필터 등록시 HTTP 요청이 들어오면
doFilter()
메서드가 호출된다. - ServletRequest, ServletResponse는 HttpServletRequest, HttpServletResponse 부모로 HTTP 요청이 아닌 경우까지 고려한 인터페이스이다.
- 필터 등록시 HTTP 요청이 들어오면
-
chain.doFilter(request, response)
- 다음 필터가 있으면 다음 필터를 호출하고 다음 필터가 없다면 서블릿을 호출한다.
- 해당 코드를 기준으로 앞쪽을 서블릿 호출 전, 뒤쪽을 서블릿 호출 후라고 볼 수 있다.
- 해당 코드가 없으면 필터를 호출하지 않고, 서블릿도 호출하지 않게 된다.
- HttpServletResponse - sendRedirect() → 해당 메서드를 호출할 경우 상태 코드는 SC_FOUND(302)가 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<jakarta.servlet.Filter>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
-
setFilter()
: 등록하고자하는 필터를 지정 -
setOrder()
: 필터 체인의 순서를 등록 -
addUrlPatterns()
: 어떤 URL 패턴에 필터를 적용할지 지정할 수 있으며 이 때, 여러 패턴 지정이 가능
- Servlet이 자바 기반의 서버 사이드 컴포넌트라면 Interceptor는 스프링 프레임워크 기반의 기술이다.
- 필터와 마찬가지로 공통 관심 사항 및 부가 기능을 담당하지만 컨트롤러 앞에서 동작하고 필터보다 더 편리하고 정교하며 더 많은 기능을 제공한다.
- 필터보다는 인터셉터를 사용하는 것을 권장한다.
- 인터셉터 역시 필터와 마찬가지로 컨트롤러 호출 전에 동작해서 적절하지 않은 요청이라면 컨트롤러를 호출하지 않고 끝낼 수 있다.
- prehandle
- 컨트롤러 호출 전에 호출
- 응답 값이 true이면 그대로 진행되고 응답 값이 false이면 더 이상 진행되지 않는다.
- posthandle
- 컨트롤러 호출 후에 호출
- afterCompletion
- 뷰가 렌더링 된 이후에 호출
→ afterCompletion()
은 예외가 발생하더라도 호출이 된다. 따라서 공통 처리를 하려면 afterCompletion()
을 호출해야 한다.
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("인증 체크 인터셉터 실행 {}", requestURI);
HttpSession session = request.getSession();
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginCheckInterceptor()) // 인터셉터 등록
.order(1) // 순서
.addPathPatterns("/**") // 등록 패턴
.excludePathPatterns("/css/**", "/*.ico", "/error"); // 제외 패턴
}
}
- 적용 사례 - JWT 토큰 기반 인증 방식에서 현재 유저를 식별할 수 있도록 하는 어노테이션 개발 경험
- 인증 / 인가를 필요로 하는 API를 요청할 경우 기존에는 서블릿에서 지원해주는 HttpSession을 찾아와 Member를 조회했으나 이 코드가 인증 / 인가를 필요로 하는 API를 요청하는 컨트롤러마다 추가해주어야 하기 때문에 코드의 중복이 발생한다.
@Target(ElementType.PARAMETER) // 파라미터에만 적용
@Retention(RetentionPolicy.RUNTIME) // 리플렉션 등을 활용할 수 있도록 런타임까지 정보가 남음
public @interface Login {
}
- 이전에도 공부를 했었지만 우리가 편리하게 사용하는 어노테이션인
@RequestBody
,@RequestMapping
등등은 모두 ArgumentResolver에 의해 동작이 된다. - 우리가 개발한 커스텀 어노테이션을 사용하려면 ArgumentResolver를 만들어서 추가해주어야 한다.
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
- supportsParameter
- LoginMemberArgumentResolver가 어떤 파라미터를 지원하는가?
-
parameter.hasParameterAnnotation
: 파라미터에 해당 어노테이션이 붙어있는가? -
isAssignableFrom
: 파라미터 타입을 체크
- resolveArgument
- 구현하고자하는 실질적인 로직을 구현하는 곳으로 여기선 서버 측 세션 저장소에 있는 로그인 멤버를 가져와서 반환한다.
-
supportsParameter
가 true가 나온 경우 resolveArgument가 동작하게 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
// ...
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}