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

ys·2024년 1월 11일

Spring-mvc2

목록 보기
7/10

김영한 강사님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요

  • 저번시간까지 Session을 이용해 로그인 한 사용자만, 상품 관리 페이지에 들어갈 수 있게 잘 만들었다
  • 그런데,,, 로그인 하지 않은 사용자가 상품 관리 페이지의 url을 알아낸 후 바로 그 url로 이동하면..? -> 들어가진다
  • 이를 막기 위해, 모든 컨트롤러에 로그인 여부를 체크해 로그인되지 않았다면 로그인 페이지로 redirect하면 되겠지만
  • 페이지가 엄청 많으면 귀찮고, 실수가 날 수 있다(= 유지 보수가 힘들다)
  • 공통관심사 : 애플리케이션 여러 로직에서, 공통으로 관심이 있는 것
  • 수정,등록,삭제,조회 등등.. 유지 보수 측면에서 모든 코드에 넣는게 아니라 공통관심사로 한번에 처리해버리자!!!
  • AOP로???
  • 웹과 관련된 공통 관심사는 필터나, 인터셉터로 하자!

    AOP에는 없는 HTTP헤더, URL정보들이 필터, 인터셉터에는 들어있기 때문에, 과 관련된 공통 관심사서블릿 필터스프링 인터셉터를 이용하자

필터와, 인터페이스 모두 인터페이스이므로 구현해서 사용해야 한다

서블릿 필터

  • 필터 : 서블릿이 지원하는 수문장
  • Filter인터페이스를 구현해서 사용해야 한다
  • 우리는 스프링을 사용하니까, 여기서 말하는 서블릿은DispatchServlet이다
  • 필터 흐름 : HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
  • 필터 제한 :
    • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
    • HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자
  • 필터 체인
    • HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
  • 필터 인터페이스
  • init() : 싱글톤 객체 생성
  • doFilter() : 고객 요청 -> 필터 로직
  • destory() : 컨테이너 종료 -> 필터도 종료

모든 기능에 로그를 찍는 코드를 구현 해보자

@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를 구현한 LogFilter라는 파일을 만든다
  • init, doFilter, destroy메서드를 오버라이드 한다
  • doFilter을 보자!!!
  • 처음 서블릿 설계가 http요청 뿐만 아니라 모든 요청도 받을 수 있게 설계되었기 때문에 httpServletRequest의 부모인 ServletRequest를 파라미터로 받는다
  • 우리는 httpServletRequest를 사용하기 때문에, downCasting을 해준다
  • randomUUID를 이용해, 임의의 토큰 아이디를 생성하고
  • 로그를 찍는다.
  • chain.doFilter(request,respone)을 호출해, 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다
  • 이단계를 생략하면 다음 단계로 진행되지 않는다
  • 오류부분도 던져주고
  • response로그도 찍어준다

WebConfig - 필터 설정

  • 만든 필터를 사용할려면, 필터 또한 설정을 해줘야 한다
  • 스프링 부트의 FilterRegistrationBean 을 사용해서 등
    록하면 된다.
  • setFilter(new LogFilter()) : 등록할 필터를 지정한다.
  • setOrder(1) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
  • addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.
  • Filterservlet이 지원하기 때문에, 스프링에서 사용하려면 @Bean 등록은 해줘야 한다
@Bean
    public FilterRegistrationBean logFilter(){
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;
    }

서블릿 필터 - 인증 체크

  • 드디어 우리가 만든 페이지의 인증 체크 필터를 개발해보자
  • 로그인 하지 않는 사용하는 페이지에 접근 못하게!
@Slf4j
public class LoginCheckFilter implements Filter {

    private 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_NUMBER)==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);
        }
    }

    /**
     * 화이트 리스트의 경우 인증 체크 X
     */
    private boolean isLoginCheckPath(String requestURI){
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
        // 하이트리스트에 안드는거 false
    }

}
  • 아까도 말했듯, HttpServletRequest, HttpServletResponse로 다운 캐스팅해준다
HttpSession session = httpRequest.getSession(false)
  • 세션을 생성하지 않고 getSession 가져온다
  • 이때, 세션이 세션쿠키로 저장되지 않아서,,, 세션이 없어 ->null이라면???
httpResponse.sendRedirect("/login?redirectURL="+requestURI);
  • 로그인 페이지로 sendRedirect를 이용해 리다이렉트 해버린다
chain.doFilter(request,response);
  • 마지막에 까먹지말고 doFilter해주기
  • finally로 인증필터 종료 log찍어주기
  • logback mdc로 같은 요청이면 로그에 같은 식별자로 나오게 하는 방법도 있다
  • 그리고 whitelist로 검사를 안할 페이지를 정해주고, requestURIPatternMatchUtils.simpleMatch를 통해, 로그인 로직을 검사해준다!!!

WebConfig - loginCheckFilter() 추가

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

        return filterRegistrationBean;
    }

이제 로그인 없이, 로그인이 필요한 페이지에 접근하면 -> 필터 공통관심사 처리에 의해서 -> 다시 로그인 페이지로 sendRedirect된다.


인터셉터

  • 스프링 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.
  • 스프링이 제공하는 기술이다
  • 스프링 인터셉터 흐름
    HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
  • 스프링 MVC가 제공하므로 디스패처 서블릿 이후에 등장한다
  • 필터와 다르게 서블릿과 컨트롤러 사이에 들어있다
  • 스프링 인터셉터 또한 URL 패턴을 적용할 수 있다
  • 서블릿 Filter 패넡과 다르게 매우 정밀하게 설정할 수 있다

  • 이렇게 생겼고..
  • 서블릿 필터의 doFilter()와 다르게 인터셉터는 호출전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)로 분화되있다
  • 인터셉터는 Handler정보와 modelAndView까지 응답 정보로 받을 수 있다

  • 정상 흐름
  • preHandle : 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.)
    • preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않는다. false 인경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 그림에서 1번에서 끝이 나버린다.
  • postHandle : 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.)
  • afterCompletion : 뷰가 렌더링 된 이후에 호출된다.

  • 예외가 발생시
  • preHandle : 컨트롤러 호출 전에 호출된다.
  • postHandle : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.
  • afterCompletion : afterCompletion 은 항상 호출된다. 이 경우 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.
  • afterCompletion은 예외가 발생해도 호출된다.
  • 예외가 발생하면 postHandle() 는 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion() 을 사용해야 한다.
  • 예외가 발생하면 afterCompletion() 에 예외 정보( ex )를 포함해서 호출된다.

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

  • 로그인 기능은 preHandler만 오버라이드 하면 된다
  • 모두 default가 붙은 메서드기 때문에, 굳이 모두 구현하지 않아도 된다
@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_NUMBER)==null){
            log.info("미인증 사용자 요청");
            // 로그인으로 리다이렉트
            response.sendRedirect("/login?redirectURL="+requestURI);
            return false;
        }

        return true;
    }
  • HandlerInterceptor를 구현한다!!!
  • 인터셉터HttpServletRequest,HttpServletResponse가 파라미터라 다운 캐스팅 안해도 된다
  • 요청 메시지에서 uri를 받고, 로그 한번 찍어준다
  • 요청 메시지에서 getSession을 통해 세션이 있는지 확인한다
  • 만약 로그인이 되있다면, 로그인 정보가 세션으로 저장되있고 서버로 세션 ID가 쿠키로 전달되었을 것이다.
  • 만약 Session이 null이라면 -> 미인증 사용자라는 것이고
  • sendRedirect로 로그인 페이지로 리다이렉트하고
  • false를 반환해 컨트롤러 자체를 실행하지 않는다
  • 그런데, whitelist같은 것들은???

SRP!!! 기능과 책임을 나누자, 인터셉터에서는 오직 세션이 있는지만 확인한다

  • addPathPatternsexcludePathPatterns으로 url을 추가 및 제외 한다
  • @Configuration 있는 webConifg파일을 봐보자
@Override
    public void addInterceptors(InterceptorRegistry registry) {
        
        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/add", "/login",
                        "/logout", "/css/**", "/*ico", "/error");
    }
  • 먼저 WebMvcConfigurer를 구현한다!!!
  • 그다음 메서드 중 addInterceptors메서드를 오버라이드 한다
  • 파라미터 registry에 addInterceptor로 방금 만든, 인터셉터 객체를 생성해 추가해준다
  • 인터셉터는 체이닝 방식을 이용하는데
  • order : 로 순위를 정하고
  • addInterceptors : 로 모든 경로를 추가하고
  • excludePathPatterns : 로 로그인 로직을 적용하지 않는 주소를 적어준다
  • 스프링 인터셉터가 개발자 입장에서 훨씬 편리하다
  • 특별한 문제가 없다면 인터셉터를 사용하자!!!

ArgumentResolver 활용

  • 우리가 스프링의 핸들러에서 여러가지 많은 파라미터를 사용할 수 있었던 이유가 바로 Argument Resolver가 있기 때문이다
  • 정확히는 HandlerMethodArgumentResolver 인데 줄여서 ArgumentResolver 라고 부른다.
  • ArgumentResolversupportsParameter() 를 호출해서 해당 파라미터를 지원하는지 체크하고,
  • 지원하면 resolveArgument() 를 호출해서 실제 객체를 생성한다. 그리고 이렇게 생성된 객체가 컨트롤러 호출시 넘어가는 것이다
    1. 인터페이스니까 확장해서 사용해야지
    2. supportsParameter(), resolveArgument() 두개만 오버라이드해서 사용하면 되겠다!!!
  • 그럼 한번 구현해서 확장한 후에, 컨트롤러에 넣어서 사용해볼까???

먼저 에노테이션 @Login을 만든다!

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
  • @Target(ElementType.PARAMETER) : 파라미터에만 사용
  • @Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있음

다음은 HandlerMethodArgumentResolver을 구현한 LoginMemberA rgumentResolver이다

@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 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_NUMBER);

    }
}
  1. HandlerMethodArgumentResolver를 구현한 LoginMemberArgumentResolver를 만든다
  2. supportsParameter(), resolveArgument() 두개만 오버라이드하자
  • supportsParameter() : @Login 애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver가 사용된다.
  • resolveArgument() : 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다.
    • 여기서는 세션에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다.
    • 이후 스프링MVC는 컨트롤러의 메서드를 호출하면서 여기에서 반환된 member 객체를 파라미터에 전달해준다
  • hasParameterAnnotation으로 @Login에노테이션이 있는지 확인하고
  • isAssignableFrom 로 파라미터의 타입이 parameter.getParameterType() 같은지 확인 한 후에
  • 두조건이 모두 참이면 true를 반환해준다
  • 이제 getNativeRequest로 다운캐스팅해 HttpSerlvetRequest를 받고, 안에서 getSession으로 저장된 Member변수를 반환한다

  • 이제 @Configuration으로 설정정보를 저장하던, webConfig파일에
    WebMvcConfigurer을 구현하고
  • addArgumentResolver을 오버라이드해서, 그 안에 add를 이용해 내가 만든 LoginMemberArgumentResolver() 객체를 추가해주자!!!
  • 그럼 스프링이 @Bean으로 등록해, 파라미터로 @Login 에노테이션을 쓸 수 있게 해준다!!!
profile
개발 공부,정리

0개의 댓글