요구사항을 보면 로그인 한 사용자만 상품 관리 페이지에 들어갈 수 있어야 한다.
앞에서 로그인을 하지 않은 사용자에게는 상품 관리 버튼이 보이지 않기 때문에 문제가 없어 보인다. 그런데 문제는 로 그인 하지 않은 사용자도 다음 URL을 직접 호출하면 상품 관리 화면에 들어갈 수 있다는 점이다
상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만, 등록, 수정, 삭제, 조회 등등 상품 관리의 모든 컨트롤러 로직에 공통으로 로그인 여부를 확인해야 한다. 더 큰 문제는 향후 로그인과 관련된 로직이 변경 될 때 이다. 작성한 모든 로직을 다 수정해야 할 수 있다
이렇게 애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern)라고 한다. 여기서는 등록, 수정, 삭제, 조회 등등 여러 로직에서 공통으로 인증에 대해서 관심을 가지고 있다.
이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만, 웹과 관련된 공통 관심사는 지금부터 설명할 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다. 웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들 이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest
를 제공한다.
필터는 client -> was 다음으로 서블릿(디스패처 서블릿)이 오기 전에 온다.
그렇기 때문에 서블릿을 갈지 말지를 정하는데 매우 용이하다.
또한 서블릿에 가지 않더라도 로그를 찍을 수도 있기 때문에 좋다.
필터는 하나만 실행되는 것이 아니라 연쇄적으로 실행 될 수 있기 때문에 첫 필터에서 로그를 찍고 두번째 세번째 필터에서 서블릿을 갈지 말지를 결정하면 된다.
우리는 이번에는 2가지 필터를 만들 것이다.
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@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);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
로그 필터는 filter가 호출 되면 URL과 로그를 구별하기 위한 UUID를 생성하고 로그를 찍는다.
그리고 chain.doFilter로 다음 단계로 넘어간다.
여기서 만약 더 이상 필터가 없다면 서블릿으로 이동한다.
다음 필터로는 로그인을 체크하는 필터이다.
@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);
}
}
로그인 체크 필터는 먼저 로그인이 필요없는 화이트리스트 URL들을 모아놓고 검증한 뒤 로그인 필요한 URL 요청인 것이 확인 되면 session이 존재하는지 확인한다.
세션이 존재하지 않거나 세션이 존재하지만 메모리에 해당 데이터가 존재하지 않는 경우에는 미인증 사용자의 요청으로 판별하고 login으로 리다이렉트 한다.
여기서 /login의 파라미터로 원래 있던 곳의 URL을 넘겨준다.
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL;
}
그리고 서블릿에서 파라미터를 받아 로그인 후 다음 리다이렉트 URL로 지정한다.
이로써 로그인 전에 하려고 했던 요청이 다시 자동으로 보내진다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
// @Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
마지막으로 WebMvcConfigurer를 구현해서 등록을 해준다.
FilterRegistrationBean를 사용해서 필터를 등록해준다.
setFilter로 원하는 로그를 등록하고 setOrder로 순서를 정한다.
그리고 원하는 URL 패턴을 등록하면 끝이다.