SpringInterCeptor

허정현·2024년 9월 16일

Spring boot

목록 보기
3/4
post-thumbnail

스프링 인터셉터를 사용하여, 비회원 유저는 특정 URL만 이용할 수 있도록 구현해야한다. 이번 포스팅 또한 사용에 대해 중점


📌 스프링 인터셉터

사용목적

  • 클라이언트의 요청에 대해 특정 검증 로직을 수행
    : 예를 들어 사용자가 회원인 경우에 가능한 기능이 있고, 비회원인 경우 가능한 기능이 있다. 이 때 요청 헤더에 담겨오는 Session등의 정보가 유효한지 검증을 통해 구분한다.
  • 특정 URL 패턴에 대해 동작하는 로직을 공통적으로 작성
    : 사용자가 관리자에 접근할 때, /admin/** 등의 관리자 권한이 있는지 확인하거나, 글쓰기 등의 URL에 접근할 경우 회원인지 확인한다.
  • 요청 처리 후에 로그를 남긴다.
    : /add/item등의 사용자가 접속한 URL에 대해 응답 상태 등을 로그로 남긴다.

흐름

링크텍스트

설명

  • 클라이언트로부터 특정 URL에 대한 접근 요청을 디스패처 서블릿에서 받게 된다. 여기서, 해당 URL이 인터셉터가 적용되도록 등록되어 있다면, HandlerInterceptor의 PreHandle이라는 메서드로 전달되게 되는데, 이 과정에서 해당 URL에 대한 접근 권한이나 로그인 검증등을 수행한다.
  • 해당 로직을 처리한 후 이 요청에 맞는 Controller에 전달하게 되고, 컨트롤러에서 로직을 처리한다. 그 후 PostHandle 전달되게 된다.
  • postHandle은 컨트롤러 로직이 수행된 후 뷰를 렌더링 하기 전에 수행되는 메서드인데, 응답에 추가 데이터를 넣거나, 요청 처리 후 로깅을 수행한다.
  • 그 후 클라이언트는 해당 요청에 대한 뷰를 렌더링받게 되고, 응답을 완료받게 된다.

사용

  • preHandle
 @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return HandlerInterceptor.super.preHandle(request, response, handler);
}

preHandle은 컨트롤러 로직을 실행하기 전에 요청을 검사하는데, 해당 URL이 인터셉터가 적용되도록 등록된 경우 권한 , 로그인 검증을 수행한다.

  • postHandle
@Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    `

컨트롤러 로직을 수행한 후 사용자에게 뷰를 렌더링하기 전에 수행하는 메서드로, 로직을 추가하거나 로그등을 남길 수 있음.

  • afterCompletion
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

뷰 렌더링 이후 처리하는 메서드이다. 이제 위의 메서드를 실행시키려면 인터셉터에 특정 URL등을 적용시키는 설정이 필요하다.

📌 Web Config

  • WebMvcConfigurer를 구현시키기 위한 클래스를 작성해야한다.
@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"); }
//...
}
  • addInterceptor(): 등록할 인터셉터를 추가.
  • addPathPatterns(): 인터셉터를 적용할 URL 패턴을 지정
  • excludePathPatterns(): 인터셉터 적용을 제외할 경로를 지정
  • order() : 여러 개의 인터셉터가 등록되어 있을 때, 각 인터셉터의 우선순위를 지정하는 데 사용. 숫자가 낮을수록 더 높은 우선순위를 갖게 되어 먼저 실행됨.

실제 코드 적용해보기.

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();
        request.setAttribute(LOG_ID, uuid);
//@RequestMapping: HandlerMethod
//정적 리소스: ResourceHttpRequestHandler if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; //호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
}
        log.info("REQUEST  [{}][{}][{}]", uuid, requestURI, handler);
return true; //false 진행X }
@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);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }
} }

WebMvcConfigurer

@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"); }
//...
}
  • 인터셉터에 로그를 남기는 LogInterceptor를 등록시킴.
  • excludePathPatterns이외에 모든 URL에 로그를 남김.
  • 2순위로 LoginCheckInterceptor를 등록시킴.
  • 사용자 인증체크를 하는 로그로 회원가입, 로그인, 로그아웃, css이외에는 전부 세션을 통해 인증체크를 해야함.

preHandler

@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("미인증 사용자 요청");
            //로그인으로 redirect
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }

        return true;
    }
}
  • preHandle 메서드를 통해 사용자의 요청이 컨트롤러로 넘어가기 전에 해당 메서드를 실행시켜 인터셉터를 실행함.
  • excludeInterceptor외에 addPathPatterns에 등록된 URL로 클라이언트에서 요청시 preHandle 메서드가 동작되어 사용자 인증체크를 실행한다.
  • request.getRequestURI(); 메서드를 통해 사용자가 요청한 URI를 가져온다.
  • request.getSession(); 메서드를 통해 요청 헤더에 세션이 있는지 없는지 검증이 시작된다.
  • session이 null이거나 SessionConst.LOGIN_MEMBER 세션에서 로그인한 사용자 정보를 저장한 키 값을 가져와 null이라면 다시 로그인 페이지로 보낸다.
  • SessionConst.LOGIN_MEMBER은 로그인 성공 시, 이 값에 로그인된 사용자 정보(사용자 ID나 객체 등)이 저장된다. 키로 SessionConst.LOGIN_MEMBER를 전달, 값으로 로그인에 성공한 멤버를 Session에 저장한다.
//로그인 성공 처리

//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();

//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

Controller

    public String homeLoginV3Spring(
            @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {

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

        //세션이 유지되면 로그인으로 이동
        model.addAttribute("member", loginMember);
        return "loginHome";
    }
  • @SessionAttribute
    @SessionAttribute는 세션에서 특정 속성을 바로 가져오는 어노테이션
    name = SessionConst.LOGIN_MEMBER: 세션에서 SessionConst.LOGIN_MEMBER라는 이름으로 저장된 객체를 찾는데, 이 값은 보통 로그인된 사용자 정보를 담고 있는 세션 속성임.
    required = false: 이 속성은 필수가 아님을 의미하기 때문에 즉, 세션에 LOGIN_MEMBER가 없을 수도 있다. 만약 세션에 값이 없으면 null이 전달되게 됨.
  1. Member loginMember
    세션에서 가져온 로그인된 사용자 정보가 loginMember 객체에 바인딩 됨. 이 값이 null이라면 사용자가 로그인되지 않은 상태
  2. Model model
    Spring MVC의 Model 객체로, 컨트롤러가 뷰(View)에 데이터를 전달할 때 사용. 여기서는 로그인된 사용자의 정보를 model에 담아서 뷰로 전달하고, 이 정보가 담긴 model을 통해 사용자의 정보를 웹에서 보여줄 수 있음.

📌 정리

  • SpringInterceptor를 사용한다면 복잡성이 증가하는 느낌이긴 하다.
  • Controller에서 잘 처리할 수 있도록 주의해야한다.
  • InterCeptor가 잦을 경우 처리 속도에 있어 느려질 수도 있다.
    (동작 과정을 보면 Controller 전, Controller후, 뷰 렌더링 이후 등이 있기 때문 )
  • prehandle 등을 통해 인증 등의 요청이 필요한 경우 처리하기 용이
  • 경로 설정에 있어서 편리
  • 인증과 로깅처리에 관심사를 처리하고, 컨트롤러에서는 완료된 데이터를 토대로 비즈니스 로직에만 집중할 수 있어 편리하다.
  • 이걸 작성하니 피곤해졌다.
profile
지식 정리를 잘 할 수 있을 때까지

0개의 댓글