공통 관심 사항
현재 우리의 상황은 아이템을 등록하고, 판매하는 웹 사이트를 만들고 있는 상황이다.
해당 사이트는 로그인을 한 사용자만 물건을 등록하거나 판매할 수 있다.
그러면, 비로그인 사용자는 물건을 등록하는 페이지에 들어갈 수 없도록 막아야 한다.
또한, 로그인한 사용자만 상품 관리 페이지에 들어갈 수 있도록 해야 한다.
문제는, 비로그인 사용자도 해당 페이지의 URL을 직접 호출하면 들어갈 수 있다는 것이다.
해당 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성해도 되지만, 로그인의 여부는 여러 컨트롤러에 공통되게 적용되는 사항이다.
때문에, 이렇게 여러 로직에서 공통으로 관심이 있는 것을 공통 관심사(cross-cutting-concern)이라고 한다.
이 문제를 Filter와 Interceptor를 사용해서 해결해보도록 하자.
서블릿 필터
필터는 쉽게 말해 서블릿이 지원하는 수문장과 같다.
필터를 적용하면 필터가 호출된 후 서블릿이 호출된다.
(스프링을 사용하는 경우에는, 해당 서블릿 = 디스패처 서블릿)
필터 흐름
HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
때문에 여기서 필터를 통해, 적절하지 않은 요청을 제한할 수 있다.
필터 제한
HTTP 요청 → WAS → 필터 (적절하지 않은 요청이라 판단, 서블릿 호출X) // 비로그인 사용자
필터는 체인으로 구성되는데, 중간에 필터를 자유롭게 추가할 수 있다.
즉, 여러 필터를 만들고 필터들 사이의 순서를 매겨 실행시킬 수 있다는 것이다.
EX) 필터1 → 필터2 → 필터3 → 서블릿
필터 인터페이스
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 : 고객의 요청이 올 때마다 해당 메서드가 호출됨. 해당 메서드에 필터 로직을 구현하면 됨.
destroy : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출됨.
필터를 만들어서 해당 메서드들을 오버라이딩하면 된다.
인증 체크 필터 개발
비로그인 사용자는 상품 관리 뿐만 아니라, 미래에 개발될 페이지에도 접근하지 못하도록 하자.
<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 {
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);
}
}
현재 필터 클래스는 doFilter() 메서드만 사용하였다.
화이트 리스트를 만들어서, 해당 리스트에 들어가는 경우에는 Filter의 인증 체크를 안하도록 설정한다.
필요에 따라 log를 통해 진행 상황을 확인해볼 수 있다.
doFilter 로직 안에 request와 response를 받아서 필요 로직 등을 실행하고, 다음 필터로 넘겨주는 코드가 필요하다.
해당 코드가 chain.doFilter(request, response) 이다.
앞서 말했듯이, 필터들은 체인들로 연결되어 있어서 다음 필터로 보내주지 않으면 오류가 발생하면서 서버가 제대로 작동하지 않는다.
return;
→ 미인증 사용자는 로그인으로 리다이렉트. 서블릿과 컨트롤러를 호출하지 않도록 한다. (불필요한 호출을 줄여줌)
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
→ 미인증 사용자를 로그인으로 리다이렉트. 그런데, 로그인 이후 다시 홈으로 보내버리면, 사용자 입장에서 다시 해당 페이지를 또 찾아서 들어가야 하므로 귀찮아진다. 이를 방지하기 위해서, 로그인 후 바로 해당 페이지로 리다이렉트하도록 설정한다.
WebConfig - LoginCheckFilter 추가
필터를 생성했으니, 이를 등록해보자.
<WebConfig>
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new
FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
setFilter() : 로그인 필터를 등록한다.
setOrder() : 순서를 2번으로 정했다. (앞에 로그 필터를 1번으로 적용했음) 로그 필터 이후 로그인 필터가 적용된다.
addUrlPatterns() : 모든 요청에 로그인 필터를 적용한다.
참고
필터는 스프링 인터셉터가 제공하지 않는 기능이 존재한다.
chain.doFilter(request, response)를 호출해서 다음 필터 또는 서블릿을 호출할 때, request와 response를 다른 객체로 바꿀 수 있다.
ServletRequest, ServletResponse를 구현한 다른 객체를 만들어서 넘기면 해당 객체가 다음 필터나 서블릿에서 사용된다.
스프링 인터셉터
웹과 관련된 공통 관심 사항을 해결하는 기술
서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.
필터와는 적용되는 순서, 범위, 사용 방법이 다르다.
스프링 인터셉터 흐름
HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
스프링 인터셉터는 MVC에서 제공하는 기능이므로, 디스패처 서블릿 이후에 등장할 수 밖에 없다.
스프링 인터셉터 역시, 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수 있다.
스프링 인터셉터 제한
HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 (적절하지 않은 요청이라 판단, 컨트롤러 호출 X)
스프링 인터셉터 역시 체인으로 구성된다.
때문에 위의 필터와 같이 인터셉터1 → 인터셉터 2 와 같이 자유롭게 추가할 수 있다.
스프링 인터셉터 인터페이스
스프링 인터센터를 사용하려면, HandlerInterceptor 인터페이스를 구현하면 된다.
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {}
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 {}
}
default로 되어있기 때문에, 모두 다 오버라이딩할 필요 없다.
서블릿 필터의 경우 doFilter() 하나만 제공한다.
그러나, 인터셉터는 호출 전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)와 같이 세분화 되어 있다.
또한, 서블릿 필터의 경우 request, response만 제공했지만, 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보도 확인할 수 있다. 그리고 어떤 modelAndView가 반환되는지 응답 정보도 확인이 가능하다.
preHandler는 boolean 형태로 true면 다음으로 진행하고, false면 더는 진행하지 않는다.
그러면 postHandle과 afterCompletion의 차이를 모르겠다는 생각을 할 수도 있다.
스프링 인터셉터의 예외 상황을 보여주며, 존재의 이유를 설명하겠다.
컨트롤러에서 예외가 발생하게 되면,
postHandle()은 호출되지 않는다.
그러나, afterCompletion()은 항상 호출된다.
이 경우 예외(ex)를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 확인이 가능하다.
인증 체크 인터셉터 개발
서블릿 필터에서 사용했던 인증 체크 필터 기능을 인터셉터로 개발해보자.
<LoginCheckInterceptor>
@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(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER)
== null) {
log.info("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
로그인을 확인하는 기능이므로 postHandle이나 afterCompletion은 필요가 없어서, preHandle() 메서드만 구현하였다.
if문을 사용해서 미인증 사용자는 로그인 화면으로 리다이렉트하면서, return false; 를 호출하여 다음 컨트롤러를 호출하지 않도록 한다.
또한, 필터에서 사용했던 화이트 리스트와 같은 코드를 구성하지 않았다.
그렇다면 어떻게 인터셉터를 적용할 페이지와 아닌 것들을 구분할 수 있을까?
이는 Config에서 편리하게 설정할 수 있다.
WebConfig - LoginCheckInterceptor 추가
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns(
"/", "/members/add", "/login", "/logout",
"/css/**", "/*.ico", "/error"
);
}
//...
}
우선, WebMvcConfigurer 를 implements해야한다.
그리고 addInterceptor() 를 오버라이딩해서 로그인 체크 인터셉터를 추가한다.
필터와 마찬가지로, order를 통해서 순서를 정한다.
addPathPatterns을 통해서 모든 URL에 적용시킨다고 설정한다.
그리고 excludePathPatterns을 사용해서, 인터셉터를 적용하는 것을 제외할 페이지들을 지정한다.
이를 통해, 화이트 리스트를 만들 필요 없이, 편리하게 제외할 페이지를 추가할 수 있다.
참고
HandlerMethod를 통해서 호출할 컨트롤러 메서드의 모든 정보를 확인할 수 있다.
@RequestMapping으로 요청이 오게 되면, HandlerMethod가 사용된다.
이 경우, 아래의 코드를 통해서 hm에 들어있는 컨트롤러 메서드의 정보를 확인할 수 있다.
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
}
다음 시간에는 ArgumentResolver를 사용해서 앞서 작성한 코드들을 더 편리하게 바꾸도록 하겠다.
'Spring' 카테고리의 다른 글
스프링 메세지 소스 및 국제화(MessageSource, Locale) (0) | 2023.12.14 |
---|---|
Spring - ArgumentResolver 활용 (0) | 2023.09.26 |
Spring- 쿠키, 세션(Cookie, Session)[로그인] (0) | 2023.09.18 |
컴포넌트 스캔 (@ComponentScan) (0) | 2023.08.02 |
싱글톤 패턴, 싱글톤 컨테이너 (Singleton) (0) | 2023.08.02 |