HttpSession 으로 클라이언트의 세션을 생성하고, session.setAttribute(User) 를 통해 세션에 유저를 저장하였다. 그리고 HomeController 에서 @SessionAttribute 로 유저를 가져와, 로그인된 유저에게 다른 뷰를 제공할 수 있게되었다. 그런데 로그인 하지 않은 사용자도 URL 을 통해 로그인한 사람만 접근 가능한 상품관리 등의 페이지에 접근이 가능하다. 상품관리 등 모든 페이지에 일일이 로그인 확인 로직을 추가해도 되겠지만 이는 매우 번거롭다. 이러한 웹 관련 공통 관심사 처리는 필터나 인터셉트를 활용한다.
Session 으로 로그인 유지는 가능해졌다. 그런데 로그인한 사람만 접근하도록 제어는 불가능하다. 이는 필터나 인터셉터로 처리한다.
HTTP 요청 -> WAS -> 필터 -> 디스패쳐 서블릿 -> 컨트롤러들
필터는 서블릿이 지원하는 수문장이다. 필터는 디스패쳐 서블릿 호출전에 호출된다. 또한 필터 체인을 통해 여러 필터를 호출할 수도 있고, 서블릿을 호출하지 않도록 중단시킬 수도 있다.
필터를 사용하려면 Filter 인터페이스를 구현하고, FilterRegistrationBean 에 등록하면된다.
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 에서 ServletRequest 와 ServletResponse 객체로 세션을 확인해 로그인 검증을 수행하고, 성공하면 chain.doFilter 로 다음 필터/서블릿을 호출한다.
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
그 후 Configuration 클래스를 만들어서 FilterRegistrationBean 을 빈으로 등록하고. 그 안에 setFilter 로 필터를 추가하면 된다.
스프링 인터셉터도 서블릿 필터와 같이 웹 관련 공통 관심사를 효과적으로 해결할 수 있다. AOP도 처리 가능하지만 웹 관련 관심사는 request, response 객체를 사용 가능한 필터나 인터셉트를 사용한다.
HTTP 요청 -> WAS -> 필터 -> 디스패쳐 서블릿 -> 인터셉터 -> 컨트롤러
우선 인터셉터는 디스패쳐 서블릿 이후 컨트롤러 호출 직전 호출된다. doFilter 하나의 메서드만 오버라이딩하여 사용하는 필터와 달리 인터셉터는 세가지 메서드를 오버라이딩 가능하다.
인터셉터의 사용은 HandlerInterceptor 인터페이스를 오버라이딩하여 인터셉를 생성하고.
WebMvcConfigurer 에서 addInterceptros 를 오버라이딩하여 그 안에서 인터셉터를 등록하면 된다.
인터셉터 생성
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("/", "/members/add", "/login", "/logout",
"/css/**", "/*.ico", "/error");
}
로그인은 컨트롤러 호출 전 수행되므로 preHandle 만 오버라이딩하면 된다. 인터셉터는 등록시 excludePathPatterns 로 제외할 URL 을 추가할 수 있기에 whitelist 를 인터셉터 내부에 둘 필요가 없다.