본문 바로가기

Programming/스프링

[김영한 스프링 MVC 2] 로그인 처리2 - 필터, 인터셉터

서블릿 필터

 

모든 url 접근하기 전, 로그인 여부 확인이 필요함 (로그인 인증)

=> 공통 관심사 

 

공통 관심사는 스프링의 AOP로도 해결 가능하지만, 웹과 관련된 공통 관심사는 서블릿 필터/스프링 인터셉터을 권장한다.

=> HttpServletRequest  통해 HTTP의 헤더나 URL의 정보 습득 가능하기 때문


※ 공통 관심사 : 애플리케이션 여러 로직에서 공통으로 관심이 있는 것

 

 

  • 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러

 

필터 적용 시 필터 호출 후 서블릿이 호출된다

필터에 URL 패턴을 적용할 수 있다 ex) /* : 모든 요청에 필터 적용

 

※ 스프링 사용 시, 서블릿 = 스프링의 디스패처 서블릿

 

 

  • 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자

 

 

  • 체인
HTTP 요청 -> WAS -> 필터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() : 필터 종료 메서드 / 서블릿 컨테이너가 종료될 때 호출됨

 

 

 

 

서블릿 필터 - 요청 로그

 

  • 로그 필터
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(); // 요청 구분 위해 UUID 생성

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

}

 

 

  • 필터 설정
@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean logFilter(){
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter()); // 등록할 필터 지정
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*"); // 모든 URL에 적용

        return filterRegistrationBean;
    }

}

 

 

 

 

서블릿 필터 - 인증 체크

 

  • 로그인 체크 필터
public class LoginCheckFilter implements Filter {

	// 해당 URL은 필터 통과
    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); // 로그인하면 기존 URI로 돌아갈 수 있도록
                    return; // 필터 진행 끝
                }
            }

            chain.doFilter(request, response);
        } catch (Exception e){
            throw e;
        } finally {
            log.info("인증 체크 필터 종료 {}", requestURI);
        }

    }

    private boolean isLoginCheckPath(String requestURI){
        return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
    }

}

 

 

  • WebConfig
    @Bean
    public FilterRegistrationBean loginCheckFilter(){
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LoginCheckFilter());
        filterRegistrationBean.setOrder(2);
        filterRegistrationBean.addUrlPatterns("/*"); // 모든 URL에 적용

        return filterRegistrationBean;
    }

 

 

  • LoginController
    @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; //// 로그인 후, redirectURL 파라미터 있으면 해당 페이지로 이동, 없으면 홈으로

    }

 

 

 

 

스프링 인터셉터

 

웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술 (스프링 MVC가 제공)

cf) 서블릿 필터 => 서블릿이 제공하는 기술

 

 

  • 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러

 

 

인터셉터는 디스패처 서블릿 - 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다
스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장한다 (스프링 MVC의 시작점이 디스패처 서블릿)

서블릿보다 훨씬 정밀하게 URL 패턴을 설정할 수 있다

 

 

  • 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비 로그인 사용자

 

 

  • 체인
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러

 

 

  • 인터페이스
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 {}
}

 

서블릿 필터 :

doFilter() 하나만 제공

request , response만 제공

 

인터셉터 :

컨트롤러 호출 전 ( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion ) 3가지 메소드 제공
어떤 컨트롤러( handler )가 호출되는지, 어떤 modelAndView 가 반환되는지 알 수 있음 (호출, 응답 정보)

 

 

  • 호출 흐름

preHandle : 컨트롤러 호출 전에 호출 (정확히는 핸들러 어댑터 호출 전에 호출)
응답값이 tru면 다음으로 진행, false면 진행 X
postHandle : 컨트롤러 호출 후 호출 (정확히는 핸들러 어댑터 호출 후에 호출)
afterCompletion : 뷰가 렌더링된 후 호출

 

 

  • 예외 상황

 

postHandle : 컨트롤러에서 예외가 발생하면 postHandle 호출 X
afterCompletion : 항상 호출 => 예외를 파라미터로 받아 어떤 예외가 발생했는지 로그로 출력 가능

 

 

 

 

스프링 인터셉터 - 요청 로그

 

  • LogInterceptor (요청 로그 인터셉터)
@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public 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(); // 요청 로그 구분 위해 uuid 세팅

        request.setAttribute(LOG_ID, uuid); // afterCompletion 에서도 사용 위해 uuid를 request에 담음

        // @Controller, @RequestMapping => HandlerMethod
        // 정적 리소스 => ResourceHttpRequestHandler
        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("REQUEST {} {} {}" , logId, requestURI, handler);

        if (ex != null){
            log.error("afterCompletion error!", ex);
        }
    }
    
}

 

 

  • Webconfig - 인터셉터 등록
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor()) // 인터셉터 등록
                .order(1) // 호출 순서
                .addPathPatterns("/**") // 인터셉터 적용할 url 패턴
                .excludePathPatterns("/css/**", "/*.ico", "/error"); // 적용 제외할 패턴

 

필터, 인터셉터가 중복으로 처리되지 않도록 logFilter 메소드의 @Bean를 주석 처리해야 한다.

 

 

※ PathPattern

? : 한 문자 일치
* :  경로(/) 안에서 0개 이상의 문자 일치
** :  경로 끝까지 0개 이상의 경로(/) 일치
{spring} :  경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} :  matches the regexp [a-z]+ as a path variable named "spring"
{spring:[a-z]+} :  regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} :  경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처

 

 

 

 

스프링 인터셉터 - 인증 체크

 

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

}

 

서블릿 필터에 비해 비교적 간단하다.

순서, 세밀한 설정은 WebConfig에서 한다.

 

 

  • WebConfig
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error"); // interceptor 적용 제외

        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/add", "/login", "/logout",
                        "/css/**", "/*.ico", "/error");
    }

 

 

 

 

ArgumentResolver 활용

 

Argument Resolver : 컨트롤러에 요청이 들어왔을 때, 특정 요청 값을 추출, 가공해 원하는 객체로 만듦

 

  • HomeController 
    @GetMapping("/")
    public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) { ////

        //세션에 회원 데이터가 없으면 home
        if (loginMember == null) {
            return "home";
        }

        //세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }

 

 

  • Login 어노테이션
@Target(ElementType.PARAMETER) // 파라미터에 해당 어노테이션 사용
@Retention(RetentionPolicy.RUNTIME) // 리플렉션 등 활용할 수 있도록 런타임까지 어노테이션 정보 보관
public @interface Login {
}

 

 

  • Argument Resolver
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        log.info("supportsParameter 실행");

        // parameter 정보 확인
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class); // @Login 추가돼있는지
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType()); // 파라미터 타입이 Member인지

        return hasLoginAnnotation && hasMemberType; // true면 resolveArgument 메소드 실행
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        log.info("resolveArgument 실행");

        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }

        return session.getAttribute(SessionConst.LOGIN_MEMBER); // member 또는 null 반환
    }
}

 

 

  • WebConfig - Argument Resolver 등록
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }