스프링 공부 (인프런 김영한 선생님)/스프링 MVC 2편

[스프링 웹 MVC 2편] 011. 로그인 처리 - 인터셉터

2023. 8. 18. 19:15
목차
  1. 스프링 Interceptor 도입
  2. 스프링 인터셉터 처리 흐름
  3. 스프링 인터셉터 제한
  4. 스프링 인터셉터 체인
  5. 스프링 인터셉터 구현 방법
  6. 스프링 인터셉터 인터페이스를 통한 구현
  7. 요청 로그 인터셉터 구현
  8. WebConfig 에 인터셉터 등록하기
  9. 스프링의 URL 경로
  10. 인증 체크 인터셉터 구현
  11. 순서 주의, 세밀한 설정 가능
  12. ArgumentResolver의 활용
  13. WebMvcConfigurer에 설정 추가

스프링 Interceptor 도입

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

둘 다 공통관심사항 처리가 가능하다는 점에서 비슷하지만 적용되는 순서/범위/사용법은 다르다

스프링 인터셉터 처리 흐름

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

  • 스프링 인터셉터는 DispatcherServlet(프론트 컨트롤러)와 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다.
  • 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패쳐 서블릿 이후에 등장한다.
  • 스프링 인터셉터에도 urlPatterns를 적용할 수 있는데 서블릿의 urlPattern과는 다르지만 매우 정밀한 설정이 가능하다.

스프링 인터셉터 제한

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 (로그인 사용자)
HTTP 요청 -> WAS -> 필처 -> 서블릿 -> 스프링 인터셉터 (적절하지 않은 요청으로 컨트롤러 호출 X)

스프링 인터셉터 체인

HTTP 요청 -> WAS -> 필터 -> 서블릿 ->인터셉터 1 -> 인터셉터 2 -> 컨트롤러 (로그인 사용자)

여기까지는 필터와 비슷해보이는데 다음 구현을 통해 차이를 알아보자

스프링 인터셉터 구현 방법

스프링 인터셉터 인터페이스를 통한 구현

스프링 인터셉터 구현을 위해서는 다음과 같이 handlerInterceptor를 구현하면 된다!

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)와 같이 단계적으로 잘 세분화되어있다.

  • 서블릿 필터의 경우 단순히 request, response만 제공했으나 HandlerInterceptor의 경우 어떤 컨트롤러(handler)가 호출되었는지도 받을 수 있고 ModelAndView 정보까지 받을 수 있다.

  • 단 afterCompletion의 경우 예외가 발생해도 호출되기 때문에 예외 상관 없이 공통사항 처리를 위해서는 해당 메서드를 사용한다, 예외가 발생하면 그 메서드에 예외정보 ex를 포함해서 호출된다.

  • 자바로 따지면 if(preHandle ) / If Not Catched(postHandle) - preHandle의 return값이 true면 실행 / finally (afterCompletion) 라고 생각하면 될듯! (postHandle의 경우 예외 발생하지 않음 실행)

교재 상에는 예외 상황을 따로 하나의 파트로 구성해서 설명하고 있는데 여기서는 위에서 설명했으니 넘어가겠음!


요청 로그 인터셉터 구현

package hello.login.web.interceptor;  

import lombok.extern.slf4j.Slf4j;  
import org.springframework.web.method.HandlerMethod;  
import org.springframework.web.servlet.HandlerInterceptor;  
import org.springframework.web.servlet.ModelAndView;  

import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import java.util.UUID;  

@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; // 다음 컨트롤러 호출  

    }  

    @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("afterCompletionError!!" ,ex); // 오류는 그냥 {} 안써도 됨  
        }  
    }  
}

String uuid = UUID.randomUUID().toString : 필터에서 구현했던것과 같이 요청별로 로그를 구분하기 위해 UUID 타입의 변수를 생성했다.

문제는 이 변수는 preHandle() 메서드에서 선언되었기 때문에 변수 스코프가 그 메서드 이상으로 넘어설 수 없고 그렇다고 공용 필드를 사용하면 싱글턴으로 객체를 관리하는 스프링에서 큰일난다

왜냐하면 멀티 스레딩도 지원하는데 싱글턴 객체에 두개의 스레드가 동시에 접근할 경우 값의 무결성이 깨질 수 있다 더 자세히 말하면 한 작업이 처리되기 이전에 스레드에 다른 작업이 끼어들어 공용필드의 값을 변경하는 순간 제대로 된 값이 나온다는 보장이 깨지고 더 나아가면 심각한 보안 문제에 직면한다.

-> 유저 A가 작업중이었는데 갑자기 유저 B의 정보가 표기된다거나....
-> 유저 A의 결제인 50,000원을 해야하는데 유저 B의 결제인 120,000원이 결제되는 등... (망한다)
-> 이 경우에는 A의 요청이 찍혀야하는데 B의 요청 로그 ID가 개입하는 등 예기치 못한 상황을 만든다

결론은 공용 필드 만들지 말자

그 대신, request.setAttribute(LOG_ID, uuid) 를 통해 값을 전달할 수 있다. request에 담아두었기 때문에 afterCompletion에서 request.getAttribute(LOG_ID)로 찾아서 사용한다.

return true - true인 경우 정상 호출이다. 다음 인터셉터나 컨트롤러가 호출된다.

preHandle에 보면 HandlerMethod 타입의 객체를 만드는것을 알 수 있는데

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

핸들러 정보는 어떤 핸들러 맵핑을 사용하는가에 따라 달라진다

  1. @Controller, @RequestMapping을 사용하는 경우 : HandlerMethod가 넘어간다
  2. /resource/static 등 정적 리소스의 경우 ResourceHttpRequestHandler가 넘어오므로 타입에 따른 처리가 필요한데, 여기서 정적 리소스를 막아버릴 일은 없어서 HandlerMethod에 대한 처리만 진행한다.
if(handler instanceof HandlerMethod){  
    HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있음.  
}

-> 이걸 왜? 하는가?
-> 값이 제대로 들어오고 있는지에 대한 검증의 의미도 있을거라 생각한다
-> @Controller, @RequestMapping을 사용하지 않고 들어오는 경우 통과시키지 않겠다는 의미로 보인다.

이전에 Javascript만으로 값을 검증하면 PostMan을 통해 다른 이상한 요청을 보냈을때 서버에서 무조건 값 검증 한번쯤은 해줘야 한다고 했는데 그 일환으로 보여진다.


WebConfig 에 인터셉터 등록하기

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

WebMvcConfigurer가 제공하는 addInterceptors()를 사용해서 인터셉터를 등록할 수 있다.

  • registry.addInterceptor(new LogInterceptor()) : 인터셉터를 등록한다.
  • order(1) : 인터셉터의 호출 순서를 지정한다.
  • addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정한다.
  • excludePathPatterns("/css/**", "/*.ico","/error") : 인터셉터에서 제외할 패턴을 지정한다.

이전에 필터에서는 필터 구현된 클래스에서 whiteList로 지정했는데 여기서는 WebConfig에서 더 정밀하게 설정할 수 있기 때문에 이 안에서 해결한다.

스프링의 URL 경로

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html

자세한 내용은 위 링크를 참고한다 URL 형식 지정 방법이다!


인증 체크 인터셉터 구현

package hello.login.web.interceptor;  

import hello.login.web.SessionConst;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.web.servlet.HandlerInterceptor;  
import org.springframework.web.servlet.ModelAndView;  

import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpServletResponse;  
import javax.servlet.http.HttpSession;  

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

}

preHandle만 구현하면 된다 왜냐하면 다음의 인터셉터의 경우
컨트롤러를 호출하기 전에만 실행되어야 하기 때문이다.

순서 주의, 세밀한 설정 가능

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

ArgumentResolver의 활용

ArgumentResolver를 이용하여 로그인 회원을 조금 더 편리하게 조회할 수 있다.

@GetMapping("/")  
    public String homeLoginV3ArgumentResolver(@Login Member loginMember,  Model model){  
        if(loginMember == null){  
            return "home";  
        }  

        model.addAttribute("member", loginMember);  
        return "loginHome";  
    }

Annotation을 이용하여 편리하게 개발할 수 있는데, 이때 Annotation의 등록을 위해 다음과 같이 코드를 작성한다.

package hello.login.web.argumentresolver;  

import java.lang.annotation.ElementType;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  

@Target(  
        ElementType.PARAMETER  
)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface Login {  
}

@Target(ElementType.PARAMETER) : 파라미터에서만 사용하도록 범위를 제한
@Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보를 남긴다.

애노테이션의 등록을 위해 이후에 LoginMemberArgumentResolver를 생성해봐야한다

package hello.login.web.argumentresolver;  

import hello.login.domain.member.Member;  
import hello.login.web.SessionConst;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.core.MethodParameter;  
import org.springframework.web.bind.support.WebDataBinderFactory;  
import org.springframework.web.context.request.NativeWebRequest;  
import org.springframework.web.method.support.HandlerMethodArgumentResolver;  
import org.springframework.web.method.support.ModelAndViewContainer;  

import javax.servlet.http.HttpServletRequest;  
import javax.servlet.http.HttpSession;  

@Slf4j  
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {  

    @Override  
    public boolean supportsParameter(MethodParameter parameter) {  
        log.info("supportsParameter 실행");  
        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);// @Login이 있나?  
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());  
        return hasLoginAnnotation && hasMemberType; // True이면 ResolveArguemnt 아니면 실행안함  
    }  

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

supportsParameter() : @Login Annotation이 있으면서 Member 타입에 해당하는 경우 ArgumentResolver를 사용하도록 처리한다

resolveArgument() : 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성해준다.
여기서는 세션의 로그인 회원 정보인 Member 객체를 찾아서 반환한다
이후 스프링 MVC는 컨트롤러의 메서드를 호출하면서
ResolverArgument를 호출하고 반환된 Member 객체를 파라미터에 전달한다.

자세한 내용은 V1~V5까지 MVC 패턴을 구현한 글을 찾아보면 Argument Resolver 에 대한 내용을 알 수 있다.


WebMvcConfigurer에 설정 추가

@Override  
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {  
    resolvers.add(new LoginMemberArgumentResolver());  
}

다음과 같이 Custom Resolver를 WebMvcConfigurer에 추가할 수 있다.

'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 2편' 카테고리의 다른 글

[스프링 웹 MVC 2편] 012. 예외 처리  (0) 2023.08.20
[스프링 웹 MVC 2편] 010. 로그인 처리 - 필터  (0) 2023.08.18
[스프링 웹 MVC 2편] 009. 로그인 처리 - 서블릿 HTTP 세션  (0) 2023.08.17
[스프링 웹 MVC 2편] 008. 로그인 처리 - 쿠키. 세션  (0) 2023.08.16
[스프링 웹 MVC 2편] 007. Bean Validation  (0) 2023.08.15
  1. 스프링 Interceptor 도입
  2. 스프링 인터셉터 처리 흐름
  3. 스프링 인터셉터 제한
  4. 스프링 인터셉터 체인
  5. 스프링 인터셉터 구현 방법
  6. 스프링 인터셉터 인터페이스를 통한 구현
  7. 요청 로그 인터셉터 구현
  8. WebConfig 에 인터셉터 등록하기
  9. 스프링의 URL 경로
  10. 인증 체크 인터셉터 구현
  11. 순서 주의, 세밀한 설정 가능
  12. ArgumentResolver의 활용
  13. WebMvcConfigurer에 설정 추가
'스프링 공부 (인프런 김영한 선생님)/스프링 MVC 2편' 카테고리의 다른 글
  • [스프링 웹 MVC 2편] 012. 예외 처리
  • [스프링 웹 MVC 2편] 010. 로그인 처리 - 필터
  • [스프링 웹 MVC 2편] 009. 로그인 처리 - 서블릿 HTTP 세션
  • [스프링 웹 MVC 2편] 008. 로그인 처리 - 쿠키. 세션
ProgYun.
ProgYun.
인내, 일관성, 그리고 꾸준함을 담습니다.
ProgYun.
Perseverance, Consistency, Continuity
ProgYun.
전체
오늘
어제
  • 분류 전체보기
    • 칼럼
    • 일상생활
      • 월별 회고
      • 인생 이야기 (대학생활)
      • 취준
      • 운동인증
      • 제품 사용 후기와 추천
    • 스프링 공부 (인프런 김영한 선생님)
      • 스프링 핵심원리
      • 스프링 MVC 1편
      • 스프링 MVC 2편
    • 면접 준비
    • 전공
      • OOP 정리
      • Design Pattern
    • 스터디
    • English
      • Electronics(Laptop)
      • 1일 1단어

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 자존감
  • 피로그래밍
  • 대학생
  • 편입생
  • 코로나
  • 코로나19
  • ssd
  • p31
  • 자가격리
  • 컴공
  • 윈도우10
  • 오미크론
  • 일상
  • 윈도우재설치
  • 해외직구
  • 하이닉스
  • 확진자
  • mason
  • NVME
  • 포맷

최근 댓글

최근 글

hELLO · Designed By 정상우.
ProgYun.
[스프링 웹 MVC 2편] 011. 로그인 처리 - 인터셉터
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.