서블릿 필터
로그인 여부를 체크하는 로직을 하나하나 추가하는 것은 매우 번거로운 일.
이러한 공통 관심사는 spring aop 로 해결할 수 있지만, HTTP 헤더나 URL의 정보들이 필요한 웹 과 관련된 공통 관심사를 처리할 때는 서블릿 필터, 스프링 인터셉터를 이용하는 것이 좋다.
aop랑 비교해서 서블릿 필터 , 스프링 인터셉터는 웹과 관련된 부가기능이 엄청많다.
필터 흐름
Http 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
스프링을 사용한다면 여거서 서블릿은 스프링의 디스패처 서블릿으로 생각하면 된다.
필터 제한 = 필터에서 적절하지 않은 요청이라 판단하면 거기에서 끝을 낼 수 도 있다.
필터체인 = 여러개의 필터를 구성할 수 있다.
필터 인터페이스의 메소드
init = 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
doFilter = 고객의 요청이 올 때 마다 해당 메서드가 호출 된다. 필터의 로직을 구현하면 된다.
destroy = 필터 종료 메서드 , 서블릿 컨테이너가 종료될 때 호출된다.
@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");
}
}
필터를 implement 해준다음에 설정을 해주어야한다.
@Configuration
public class WebConfig
{
@Bean
public FilterRegistrationBean logFilter()
{
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
filter의 순서와 어떤 url pattern 에 적용을 시킬 것인지를 알려주는 역할이다.
현재는 random한 uuid를 로그로 남기게 되어있다.
실무에서는 HTTP 요청시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc로 검색해보자.
로그인된 사용자를 체크하는 필터를 만들자
@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
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);
}
}
로그인체크 필터를 통해 추후에 기능이 추가되더라도 모두 로그인이 되었는지 확인을 해주는 검증 필터이다. 마찬가지로 @Configuration에서 @Bean으로 등록을 해주어야 동작한다.
또한 로그인을 성공하면 로그인을 실패했던 페이지의 URI를 넘겨주기 때문에 Controller 단에서 그걸 받아서 로그인을 성공하게되면 다시 실패했던 페이지로 이동시켜줄 수 있다.
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request)
{
if(bindingResult.hasErrors())
{
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if(loginMember == null)
{
bindingResult.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//if session exist just return existing session , else return create new session
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER,loginMember);
//create session with session manager, add memeber data
// sessionManager.createSession(loginMember,response);
return "redirect:" + redirectURL;
}
@RequestParam 에게 defaultvalue를 줌으로써 아무런 URI정보가 넘어오지 않으면 홈으로 되돌아간다.
스프링 인터셉터
스프링 인터셉터가 훨씬더 많은 기능을 제공하고 좋다.
스프링 인터셉터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
여기서 서블릿은 스프링이게 디스패처 서블릿이다.
중간에 끊는 것이랑 체인 형식을 필터랑 똑같이 지원한다.
스프링 인터셉터 인터페이스
서블릿 필터의 경우 단순히 request, response만 제공했지만 , 인터셉터는 어떤 컨트롤러 handler 가 호출되는지 호출 정보도 받을 수 있다. 또한 modelAndView 가 반환되는지 응답 정보도 받을 수 있다.
preHandle = 컨트롤러 호출전에 호출 (더 정확히는 핸들러 어댑터 호출전에 호출)
응답 값이 true면 다음으로 진행하고 , false면 끝내버린다.
postHandle = 컨트롤러 호출 후에 호출된다. (정확히는 핸들러 어댑터 호출후에 호출된다.)
afterCompletion = 뷰가 렌더링 된 이후에 호출이 된다.
Controller에서 예외가 터지는 경우에는 postHandle이 호출 되지 않는다.
afterCompletion에 ex parameter에 예외가 들어 오게 된다.
@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("afterCompletion error!!", ex);
}
}
}
싱글톤으로 사용되기에 멈버 변수로 사용하면 매번 새로운 request가 올때마다 교체되어버릴 것이다.
따라서 상수 static final 을 이용하고 필요하면 request.setAttribute로 적용시켜 주면 된다.
현재 logId를 prehandle에서 afterCompletion에 전달하고싶어서 request.setAttribute를 활용하였다.
@Controller가 아니라 /reosurce/static 같은 정적리소스가 호출이 되는 경우 ResourceHttpRequestHandler가 핸들러 정보로 넘어오기 때문에 타입에 따라서 처리가 필요하다.
@Configuration
public class WebConfig implements WebMvcConfigurer
{
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**","/*.ico","/error");
}
필터 설정과는 다르게 추가적으로 WebMvcConfiguerer 를 implement하고 override해줘야한다.
/** 는 하위 모든것을 의미한다.
스프링 인터셉터 - 인증체크
@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;
}
}
똑같은 로직이지만 whitelist가 없다.
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/", "/members/add", "/login", "/logout",
"/css/**", "/*.ico", "/error");
WebConfig 에서 WebMvcConfigurer 메소드를 오버라이딩 하면서 interceptor를 등록할 때 설정할 수 있기 때문이다.
ArgumentResolver
@GetMapping("/")
public String homeLoginV3ArgumentResolver(
@Login Member loginMember, Model model)
{
if (loginMember == null)
{
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
@Login을 통해서 Session 에서 조회하고 제대로 된놈이 로그인 했는지 확인하게 하고싶다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login
{
}
파라미터에만 사용하고 리플렉션 등을 활용할 수 있도록 런타임까지 애노태이션 정보가 남아 있다.
@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;
}
Object member = session.getAttribute(SessionConst.LOGIN_MEMBER);
return member;
}
}
reflection 을 통해 Annotation 을 확인 하는 다른 프레임워크처럼 supprotParameter method가 @Login 을 확인하고 Member 클래스를 확인한다.
확인이 되었으면 resolveArgument에서 세션 확인 로직을 실행한다.
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
마찬가지로 WebConfig에 @Override 메소드 해서 등록을 해주어야 한다.
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
내부적인 캐시가 있어서 supportParameter는 제일 처음에만 실행이 된다.
'WEB > Spring MVC 2' 카테고리의 다른 글
Spring MVC 2편 API 예외 처리 (0) | 2021.10.30 |
---|---|
Spring MVC 2편 예외처리와 오류페이지 (0) | 2021.10.23 |
Spring MVC 2편 로그인 처리1 쿠키 세션 (0) | 2021.10.10 |
Spring MVC 2편 검증2 - Bean Validation (0) | 2021.09.26 |
Spring MVC 2편 검증1 - Validation (0) | 2021.09.05 |