현재 로그인 안한 사용자도 상품 관리 페이지에 접근이 가능하다.
모든 컨트롤러 로직에 로그인 여부를 체크하는 과정을 추가해야 할 까?
반복되는 기능이다. 따라서 이를 공통으로 관리할 수 있는 “서블릿 필터” 가 있다.
여러 로직에서 공통으로 관심이 있는 것을 공통 관심사라고 한다.
공통 관심사는 스프링의 AOP로 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
→ 관점 지향 프로그래밍이다.
어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고, 그 관점을 기준으로 각각 모듈화 하겠다 → 모듈화는? : 공통 로직이나 기능을 하나의 단위로 묶는것.
→ 이때, 공통으로 사용되는 로직이나 기능의 코드들을 “Crocss Cutting Concerns” 흩어진 관심사 라고 한다.
따라서, AOP로 해결할 수 있지만 → 로그인 검증에 관한 로직을 별도로 클래스화 하여 사용할 수 있지만, 웹과 관련된 공통 관심사인 로그인 검증에서는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
필터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
필터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출 하지 않음) //비 로그인 사용자
필터에서 적절하지 않은 요청이라고 판단하면, 거기에서 끝낼 수도 있다. 그래서 로그인 여부를 체크하기 좋다.
필터 체인
필터는 체인으로 구성되는데, 중간에 필터를 자유롭게 추가할 수 있다.
하나의 HTTP 요청이 들어와서 나의 애플리케이션에서 로그를 남길 때는 전부 다 자동으로 uuid를 남기고 싶다? → 로그가 남을 때 uuid를 통해 추정이 쉽다. : logback.mdc 를 사용하면 된다.
필터 인터페이스
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() {}
}
필터 인터페이스를 구현하고, 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.
필터가 정말 잘 동작 하는지, 모든 요청을 로그로 남기는 필터를 개발하고 적용해보자.
LogFilter
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
//http 요청이 들어오면 doFilter 가 실행된다.
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("log filter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try{
log.info("REQUEST [{}] [{}]", uuid, requestURI);
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}] [{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
main package → WebConfig.class 생성
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter());
filterFilterRegistrationBean.setOrder(1);
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
}
스프링 부트를 사용한다면, FilterRegistrationBean 을 사용해서 필터를 등록한다!
로그를 찍는 필터는 완료, 이제 인증 체크 필터를 개발하자. 로그인 되지 않은 사용자는 상품 관리 뿐만 아니라, 앞으로 개발될 페이지에도 접근하지 못하도록 하고싶다.
LoginCheckFilter - 인증 체크 필터
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
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);
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
/**
* 화이트 리스트인 경우 인증 체크 안하도록 하는 메서드
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
WebConfig - loginCheckFilter() 추가
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
filterFilterRegistrationBean.setOrder(2);
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
RedirectURL 처리
로그인에 성공하면 처음 요청한 URL로 이동하는 기능을 개발해보자.
LoginController - loginV4()
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request, @RequestParam(defaultValue = "/") String redirectURL) {
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;
}
/login 에 redirectURL 요청 파라미터를 추가하여 요청했고, 이를 redirect 시켜 줌.스프링 인터셉터에서도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.
서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링MVC 가 제공하는 기술이다. 둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다.
스프링 인터셉터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> ***스프링 인터셉터*** -> 컨트롤러 (핸들러)
스프링 인터셉터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터
(적절하지 않은 요청이라 판단, 컨트롤러 호출하지 않는다.)
// 비 로그인 사용자
스프링 인터셉터 체인
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
스프링 인터셉터는 체인으로 구성되는데, 중간에 인터셉터를 자유롭게 추가할 수 있다. 예를 들어 로그를 남기는 인터셉터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 인터셉터를 만들 수 있다.
스프링 인터셉터 인터페이스
HandlerInterceptor interface
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 {
}
}

정상 흐름
afterCompletion은 예외가 발생해도 호출된다.
afterCompletion() 을 사용해야 한다.LogInterceptor
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
private static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid); //중요!!
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
}
log.info("REQUEST [{}] [{}] [{}]", uuid, requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}] [{}] [{}]", logId, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
WebConfig - 인터셉터 등록
@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", "/css/**", "logout", "/*.ico", "/error");
}
}
WebMvcConfigurer 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다.
registry.addInterceptor(new LogInterceptor()) : 인터셉터를 등록한다.order(1) : 인터셉터의 호출 순서를 지정한다 - 낮을 수록 먼저 호출 된다.addPathPatterns(”/**”) : 인터셉터를 적용할 URL 패턴을 지정한다.excludePathPatterns(”/css/**”, “/*.ico”, “/error”) : 인터셉터에서 제외할 패턴을 지정한다.필터와 비교해보면, 인터셉터는 addPathPatterns, excludePathPatterns 로 매우 정밀하게 URL 패턴을 지정할 수 있다.
로그인 검증은 해당 컨트롤러 (핸들러 어댑터) 가 호출되기 전 검증을 실시하고, 해당 컨트롤러의 호출 여부를 판단해야 한다. 즉 로그인 검증에서는 preHandle 이 사용되는 것이 바람직 하다.
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();
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}
}
@Login 애노테이션을 직접 만들어 실행하자!
직접 만든 애노테이션이 있다면, 직접 만든 ArgumentResolver 가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고, 만약 세션이 없다면 null 을 반환하도록 개발하자.
@Login Annotaion
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
LoginMemberArgumentResolver
@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 methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest nativeRequest = (HttpServletRequest) nativeWebRequest.getNativeRequest();
HttpSession session = nativeRequest.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
supportsParameter() : @Login 애노테이션이 있으면서, Member 타입이며느 해당 ArgumentResolver 가 사용된다.resolveArgument() : 컨트롤러 호출 직전에 호출 되어서, 필요한 파라미터 정보를 생성해준다. 여기서는 세션에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다.HomeController
@GetMapping("/")
public String homeLoginArgumentResolver(@Login Member loginMember, Model model) {
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
WebConfig
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}