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

2024. 5. 4. 13:17·Spring/MVC 2

앞서 코드에서 세션을 이용해 로그인 기능을 구현하였다.

하지만 url을 통해 로그인을 하지 않는 사용자도 상품 관리 화면에 접근할 수 있는 문제가 있다.

 

각 컨트롤러에서 로그인 여부를 체크할 수도 있겠지만, 이 경우 로그인과 관련된 로직이 변경될 때 유지 보수성이 매우 어려워진다.

따라서 로그인 인증에 관한 '공통 관심사(cross-cutting concern)'를 설정하여 한 번에 처리하는 방법이 좋다.

→ AOP, 서블릿 필터, 스프링 인터셉터

 

 


 

1. 서블릿 필터

 

필터 흐름.

HTTP 요청 → WAS → 필터 → 서블릿(Dispatcher) → 컨트롤러

 

필터 체인으로 구성되며, 중간에 필터를 자유롭게 추가할 수 있다. ( LogFilter → LoginCheckFilter )

필터에 적절하지 않은 요청이 오면, 다음 과정인 서블릿과 컨트롤러를 호출하지 않고 해당 요청을 종료시켜야 한다! (return)

 

cf. 스프링 인터셉터는 제공하지 않는 기능 (잘 사용되지는 않음..)

chain.doFilter(request, response);

에서 다음 필터 또는 서블릿을 호출할 때, request / response 를 바꿔 넣을 수 있다.

 

 

 

[Filter]

public interface Filter {
    // 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    // HTTP 요청이 올 때마다 해당 메서드가 호출된다.
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;

    // 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
    default void destroy() {
    }
}

 

 

[LogFilter]

@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); // 다음 필터 or 서블릿을 호출해주어야 한다.
        } catch (Exception e) {
            throw e;
        } finally {
            log.info("RESPONSE [{}][{}]", uuid, requestURI);
        }
    }

    @Override
    public void destroy() {
        log.info("log filter destroy");
    }
}

 

 

[LoginCheckFilter]

@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, 로그인 이후 접속을 시도한 view로 바로 띄워주기 위해 requestURI를 포함시켜준다.
                    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);
    }
}

 

 

[WebConfig]

// 필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean 을 사용해서 등록하면 된다. (필터 순서 조절 가능)

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();

        filterFilterRegistrationBean.setFilter(new LogFilter()); // 필터 등록
        filterFilterRegistrationBean.setOrder(1); // 필터는 체인으로 동작하므로 순서가 필요하다. (낮을수록 먼저 동작)
        filterFilterRegistrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴 (여러 패턴 지정 가능)

        return filterFilterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();

        filterFilterRegistrationBean.setFilter(new LoginCheckFilter()); // 필터 등록
        filterFilterRegistrationBean.setOrder(2);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }
}

 

 


 

2. 스프링 인터셉터

Spring MVC에서 '서블릿 필터'와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있게 제공하는 기술이다.

스프링 컨텍스트 내부에서 동작하면서, 컨트롤러 레벨에서 더 세밀한 제어가 가능하기 때문에 실무에서는 인터셉터를 더 많이 사용한다.

 

  Filter Interceptor
실행시점 1) 서블릿 컨테이너가 생성될 때
2) 컨트롤러 호출하기 전
3) 서블릿 컨테이너가 종료될 때(응답 전)
1) 컨트롤러 호출하기 전
2) 컨트롤러 호출 후
3) 뷰가 렌더링 된 이후(응답 후)
스프링 의존성 주입(DI) X (스프링 컨텍스트 외부에서 동작함) O
URL 패턴 매핑 PatternMatchUtils.simpleMatch 유틸리티 사용 WebConfig에서 각 인터셉터마다 URL 패턴 매칭 가능
에러 처리 용이성 필터 수준에서의 예외 처리
(chain.doFilter 호출을 통해 다음 단계로 전달되는 과정에서 발생한 예외를 처리)
컨트롤러 수준에서의 예외 처리
로깅 및 성능 모니터링 서블릿 레벨 컨트롤러 레벨

 

 

스프링 인터셉터 흐름.

HTTP 요청 → WAS → 필터 → 서블릿(Dispatcher) → 스프링 인터셉터 → 컨트롤러

 

인터셉터 체인에 적절하지 않은 요청에 대한 컨트롤러를 호출하지 않고 해당 요청을 종료하는 판단은 '서블릿 필터'와 동일하게 동작한다.

 

 

[HandlerInterceptor]

public interface HandlerInterceptor {
    // 컨트롤러(핸들러 어댑터) 호출 전에 호출된다.
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return true;
    }

    // 컨트롤러(핸들러 어댑터) 호출 후에 호출된다.
    // 컨트롤러에서 예외가 발생하면 호출 X
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                            @Nullable ModelAndView modelAndView) throws Exception {
    }

    // 뷰가 렌더링 된 이후에 호출된다.
    // 예외(ex)를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.
    // 항상 호출된다.
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                 @Nullable Exception ex) throws Exception {
    }
}

 

 

[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 logId = UUID.randomUUID().toString();

        // request 종료 시점까지 LOG_ID로 추적하기 위함.
        // LogInterceptor가 싱글톤처럼 사용되기 때문에, 멤버변수로 사용하면 안된다.
        request.setAttribute(LOG_ID, logId);

        // RequestMapping : HandlerMethod
        // 정적 리소스 : ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
        }

        log.info("REQUEST [{}][{}][{}]", logId, requestURI, handler);
        return true; // 다음 인터셉터 or 컨트롤러(핸들러) 호출
    }

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

 

 

[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); // 로그인으로 redirect
            return false;
        }

        return true;
    }
}

 

 

[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", "/logout", "/css/**", "/*.ico", "/error");
    }
}

 

저작자표시

'Spring > MVC 2' 카테고리의 다른 글

9. API 예외 처리  (0) 2024.05.06
8. 예외 처리와 오류 페이지  (0) 2024.05.05
6. 로그인 처리1 - 쿠키, 세션  (1) 2024.05.02
5. 검증 2 - Bean Validation  (0) 2024.05.01
4. 검증 1 - Validation  (0) 2024.04.29
'Spring/MVC 2' 카테고리의 다른 글
  • 9. API 예외 처리
  • 8. 예외 처리와 오류 페이지
  • 6. 로그인 처리1 - 쿠키, 세션
  • 5. 검증 2 - Bean Validation
wch_t
wch_t
  • wch_t
    끄적끄적(TIL)
    wch_t
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (168)
      • Architecture (0)
      • Algorithm (67)
        • Math (5)
        • Simulation (1)
        • Data Structure (4)
        • DP (7)
        • Brute Fource (10)
        • Binary Search (6)
        • Greedy (2)
        • Graph (11)
        • Mst (1)
        • Shortest path (10)
        • Two Pointer (1)
        • Tsp (3)
        • Union Find (2)
        • Mitm (1)
      • CS (2)
        • 데이터베이스 (5)
        • 네트워크 (5)
      • DB (6)
      • DevOps (15)
        • AWS (9)
        • Docker (1)
        • CI-CD (5)
      • Error (1)
      • Project (0)
        • kotrip (0)
      • Spring (59)
        • 끄적끄적 (5)
        • 기본 (9)
        • MVC 1 (7)
        • MVC 2 (11)
        • ORM (8)
        • JPA 1 (7)
        • JPA 2 (5)
        • Spring Data Jpa (7)
      • Test (2)
      • TIL (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백준 17289 파이썬
    Merge
    docker: not found
    백준 3015 파이썬
    form_post
    spring-cloud-starter-bootstrap
    view algorithm
    Sxssf
    애플
    scope
    백준 17299 파이썬
    spring-cloud-starter-aws-secrets-manager-config
    docker
    response_mode
    Jenkins
    apache poi
    aws secrets manager
    TempTable
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
wch_t
7. 로그인 처리2 - 필터, 인터셉터
상단으로

티스토리툴바