1. 서블릿 필터
이전 포스팅에서 구현한 코드는 로그인을 하지 않아도 특정 url을 입력하면 해당 url로 이동할 수 있다. 이를 위해 모든 비지니스 로직을 수행할 때 사용자의 로그인 여부를 체크할 수 있다. 하지만 이런 방식은 이후 로그인 관련 로직을 바꿀 때 해당 체크 로직을 다 수정해야할 수도 있다.
- 이와 같이 여러 로직에서 공통으로 관심있는 것을 공통 관심사(cross-cutting concern)이라고 한다.
이러한 공통 관심사는 AOP로도 해결할 수 있다. 하지만 웹과 관련된 공통 관심사는 서블릿 필터 혹은 스프링 인터셉터를 사용하는 것이 좋다. 필터는 서블릿이 지원하는 수문장 역할을 하는 것으로 필터의 흐름은 다음과 같다.
- HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
- 만약 필터를 통해 적절하지 않은 요청이라고 판단하면 이후 서블릿에 전달하지 않는다.
또한 필터는 체인으로 구성되기 때문에 중간에 필터를 자유롭게 추가할 수 있다.
- HTTP 요청 → WAS → 필터1 → 필터2 → ... → 서블릿 → 컨트롤러
필터 인터페이스는 다음과 같다.
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
- 필터 인터페이스를 구현하고 등록하면, 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.
- init(): 필터의 초기화 메서드로 서블릿 컨테이너가 생성될 때 호출된다.
- doFilter(): 고객의 요청이 올 때마다 해당 메서드가 호출된다. 필터의 로직이 담긴다.
- destory(): 필터의 종료 메서드로 서블릿 컨테이너가 종료될 때 호출된다.
1.1. 요청 로그 필터
모든 요청에 대해 로그를 위해서 필터를 적용해 보았다. 먼저 필터를 구현한 코드는 다음과 같다.
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void destroy() {
log.info("log filter destroy");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("Request [{}] [{}]", uuid, requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("Response [{}] [{}]", uuid, requestURI);
}
}
}
- 필터를 사용하기 위해서는 Filter 인터페이스를 구현해야 한다.
- chain.doFilter(request, response): 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 이를 호출하지 않으면 다음 단계로 진행이 안된다.
필터를 만든 후에는 만든 필터를 등록해야 한다. 스프링 부트를 사용할 때는 FilterRegistrationBean을 사용해 등록할 수 있다.
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<Filter> logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
- setFilter(): 필터를 등록한다.
- setOrder(): 체인에서 필터의 순서를 지정한다.
- addUrlPatterns(): 필터를 적용할 URL 패턴을 지정한다.
이외에도 @ServletComponentScan이나 @WebFilter(filterName = "logFilter", urlPatterns = "/*")로도 등록할 수 있지만, 필터 순서조절이 안된다. 추가적으로 로그 ID에 대해서는 Logback mdc를 검색 후 참고하자.
1.2. 로그인 체크 필터
로그인 체크 필터를 위한 LoginCheckFilter는 다음과 같다.
@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 {
log.info("Start: Login Check Filter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
if (isLoginCheckPath(requestURI)) {
HttpSession session = httpRequest.getSession();
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("/미인증 사용자 요청 {}", requestURI);
httpResponse.sendRedirect("/login?redirectURL="+requestURI);
return;
}
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("End: Login Check Filter");
}
}
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
}
}
2. 스프링 인터셉터
스프링 인터셉터도 서블릿 필터와 마찬가지로 공통 관심사를 해결할 수 있는 기술이다. 단, 스프링 인터셉터는 스프링 MVC에서 제공하는 기능이면 적용되는 순서, 범위, 사용법이 다르다. 스프링 인터셉터의 흐름은 다음과 같다.
- HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
스프링 인터셉터는 HandlerInterceptor 인터페이스를 구현해야 한다.
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
- 서블릿 필터는 doFilter 하나만 제공된다. 하지만 인터셉터의 경우 컨트롤러 호출 전(preHandler)과 호출 후(postHandle), 요청 완료 이후(afterCompletion) 총 세 가지 단계를 제공한다.
- preHandle: DispatcherServlet에 요청이 들어오면 바로 실행된다. (리턴값이 true면 이후 과정을 진행하고, false면 나머지 인터셉터, 핸들러 어댑터 아무것도 호출되지 않는다.)
- postHandle: 핸들러 어댑터가 핸들러(컨트롤러)를 호출하고 요청을 처리(ModelAndView를 반환)하면 실행된다.
- afterCompletion: view를 Rendering한 후 호출된다.
- 핸들러(컨트롤러)에서 예외가 발생하면 postHandle은 호출되지 않는다. 하지만 afterCompletion은 예외가 터지든 말든 무조건 실행된다.
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 생각하면 된다. 따라서 특별히 필터를 사용해야 하는 경우가 아니라면 인터셉터를 사용하는 것이 편하다. 인터셉터를 구현하고 나면 다음과 같이 추가해주면 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
- *는 경로 안에서 0개 이상의 일치 경로를 말한다.
- **은 경로 끝까지 0개 이상의 일치 경로를 말한다.
2.1. 로그인 체크 인터셉터
로그인 체크 관련 인터셉터는 다음과 같이 작성할 수 있다.
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("Start: Login Check Interceptor / URI: {}", requestURI);
HttpSession session = request.getSession();
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("Not Authenticated");
response.sendRedirect("/login?redirectURL="+requestURI);
return false;
}
return true;
}
- 필터에서 사용한 whiteList의 경우 이후 인터셉트를 등록할 때 excluePathPatterns를 활용하면 된다.
3. ArgumentResolver 활용
먼저 사용할 어노테이션을 한 개 만들어준다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
그리고 이 어노테이션이 붙었을 때 활용할 ArgumentResolver를 아래와 같이 만들어준다.
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
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 {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
Object member = session.getAttribute(SessionConst.LOGIN_MEMBER);
return member;
}
}
- supportsParameter(): argumentResolver가 실행될 조건을 확인하는 메서드이다. 여기서는 @Login 어노테이션이 있고, Member 타입이면 ArgumentResolver가 사용된다.
- resolveArgument(): 컨트롤러 호출 직전에 호출되어 파라미터 정보를 생성한다. 여기서는 세션에 있는 로그인 회원 정보인 member 객체를 찾아 반환한다.
그리고 해당 ArgumentResolver를 등록해야 한다. 기존의 WebConfig 클래스에 등록하면 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
...
}
그리고 어노테이션을 사용할 컨트롤러의 메서드에 파라미터 부분에서 @Login을 사용하면 Member 객체를 바인딩 할 수 있다.
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member member, Model model) {
if (member == null) {
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'BackEnd > Spring' 카테고리의 다른 글
JWT란? (0) | 2024.07.31 |
---|---|
[Spring MVC-2] 예외 처리 (0) | 2024.06.27 |
[Spring MVC-2] 로그인 - 쿠키, 세션 (0) | 2024.05.27 |
[Spring MVC-2] Bean Validation (0) | 2024.05.24 |
[Spring MVC-2] Validation (0) | 2024.05.23 |