7. 로그인 처리2 - 필터, 인터셉터

ChoiJaewoo·2026년 2월 4일

Spring MVC 2편

목록 보기
7/10

7. 로그인 처리2 - 필터, 인터셉터

현재 로그인 안한 사용자도 상품 관리 페이지에 접근이 가능하다.

모든 컨트롤러 로직에 로그인 여부를 체크하는 과정을 추가해야 할 까?

반복되는 기능이다. 따라서 이를 공통으로 관리할 수 있는 “서블릿 필터” 가 있다.

서블릿 필터

여러 로직에서 공통으로 관심이 있는 것을 공통 관심사라고 한다.

공통 관심사는 스프링의 AOP로 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.

스프링의 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() {}
}

필터 인터페이스를 구현하고, 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.

  • init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
  • doFilter() : 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
  • 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");
    }
}
  • 필터를 사용하기 위해 Filter interface 를 구현한다
  • doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
    • HTTP 요청이 오면, doFilter가 호출된다.
    • 왜 HttpServletRequest, HttpServletResponse 가 아니고 그냥 ServletRequest, Response 일까?
      • HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이기 때문에, (HttpServletRequset) 를 사용해서 다운 캐스팅 해주어야 한다.
  • String uuid = UUID.randomUUID().toString()
    • HTTP 요청을 구분하기 위해 임의의 랜덤값을 생성한다.
  • log.info("REQUEST [{}][{}]", uuid, requestURI);
    • uuid, requestURI 를 출력한다.
  • chain.doFilter(request, response);
    • 다음 필터가 있으면, 필터를 호출하고, 없으면 서블릿을 호출한다.
    • 💡 이 로직을 호출하지 않으면, 다음 단계로 진행되지 않는다!!! 중요!!

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 을 사용해서 필터를 등록한다!

  • setFilter() : 등록할 필터 지정
  • setOrder() : 필터 체인의 순서 - 낮을 수록 먼저 동작한다
  • addUrlPatterns() : 필터를 적용할 URL 패턴을 지정

서블릿 필터 - 인증 체크

로그를 찍는 필터는 완료, 이제 인증 체크 필터를 개발하자. 로그인 되지 않은 사용자는 상품 관리 뿐만 아니라, 앞으로 개발될 페이지에도 접근하지 못하도록 하고싶다.

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);
    }
}
  • whitelist → “/” , “/members/add” , “/login” , “/logout” , “/css/*
    • 인증 필터에서 항상 허용되는 URL 을 whitelist list로 관리한다.
  • isLoginCheckPath(requestURI) : Boolean
    • PatternMatchUtils.simpleMatch(whitelist, requestURI) : match 하면 true
  • httpResponse.sendRedirect(”/login?redirectURL=” + requestURI);
    • 미 인증 사용자는 로그인 화면으로 다시 리다이렉트 하고, 로그인 이후에 다시 홈으로 이동하면 원하는 경로를 직접 다시 찾아가야 한다.
  • 만약, 로그인을 해야하는 사용자로 판단된다면,
    • httpResponse.sendRedirect(”/login?redirectURL=” + requestURI); redirect 응답을 보내고, return; 해준다. → 필터를 더 진행하지 않고, 서블릿, 핸들러(컨트롤러) 모두 호출되지 않도록 한다.

WebConfig - loginCheckFilter() 추가

@Bean
public FilterRegistrationBean loginCheckFilter() {
    FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
    filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
    filterFilterRegistrationBean.setOrder(2);
    filterFilterRegistrationBean.addUrlPatterns("/*");

    return filterFilterRegistrationBean;
}
  • setFilter(new LoginCheckFilter() ) : 로그인 필터를 등록한다
  • setOrder(2) : 순서를 2번으로 잡았다. 로그 필터 다음에 로그인 필터가 적용된다
  • addUrlPatterns(” /* ”) : 모든 요청에 로그인 필터를 적용한다

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;
}
  • 로그인 체크 필터에서, 미인증 사용자는 요청 경로를 포함하여 /loginredirectURL 요청 파라미터를 추가하여 요청했고, 이를 redirect 시켜 줌.

스프링 인터셉터 - 소개

스프링 인터셉터에서도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.

서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링MVC 가 제공하는 기술이다. 둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용방법이 다르다.

스프링 인터셉터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> ***스프링 인터셉터*** -> 컨트롤러 (핸들러)
  • 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다.
  • 스프링 인터셉터에도 URL 패턴을 적용할 수 있는데, 서블릿 URL 패턴과는 다르고, 매우 정밀하게 설정할 수 있다.

스프링 인터셉터 제한

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 {
	}
}
  • 서블릿 필터는 doFilter() 하나만 제공하였다.
  • 하지만 인터셉터는 컨트롤러 호출 전 - preHandle, 호출 후 - postHandle, 요청 완료 이후 - afterCompletion 과 같이 단계적으로 잘 세분화 되어 있다.

정상 흐름

  • preHandle : 컨트롤러 호출 전 호출됨 - true : 핸들러 어댑터 호출
  • postHandle : 컨트롤러 호출 후 호출
  • afterCompletion : 뷰가 랜더링 된 이후 호출

afterCompletion은 예외가 발생해도 호출된다.

  • 예외가 발생하면 postHandle()은 호출되지 않는다. 예외와 무관하게 공통처리를 하려면 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);
        }

    }
}
  • request.setAttribute(LOG_ID, uuid) :
    • 서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있다.
    • 따라서 preHandle에서 지정한 값을 postHandle, afterCompletion 에서 함께 사용하려면 어디에 담아두어야 하는데, LogInterceptor 도 싱글톤 처럼 사용되기 때문에 멤버변수를 사용하면 위험하다.
    • 따라서 request 에 담아두었고, 이 값은 afterCompletion 에서 request.getAttribute(LOG_ID)로 찾아서 사용한다.

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;
    }
}

ArgumentResolver 활용

@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());
}

0개의 댓글