스프링 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;
//호출할 컨트롤러 메서드의 모든 정보가 포함되어있다.
}
핸들러 정보는 어떤 핸들러 맵핑을 사용하는가에 따라 달라진다
@Controller
,@RequestMapping
을 사용하는 경우 :HandlerMethod
가 넘어간다/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 경로
자세한 내용은 위 링크를 참고한다 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 |
스프링 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;
//호출할 컨트롤러 메서드의 모든 정보가 포함되어있다.
}
핸들러 정보는 어떤 핸들러 맵핑을 사용하는가에 따라 달라진다
@Controller
,@RequestMapping
을 사용하는 경우 :HandlerMethod
가 넘어간다/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 경로
자세한 내용은 위 링크를 참고한다 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 |