서블릿 필터
모든 url 접근하기 전, 로그인 여부 확인이 필요함 (로그인 인증)
=> 공통 관심사
공통 관심사는 스프링의 AOP로도 해결 가능하지만, 웹과 관련된 공통 관심사는 서블릿 필터/스프링 인터셉터을 권장한다.
=> HttpServletRequest 통해 HTTP의 헤더나 URL의 정보 습득 가능하기 때문
※ 공통 관심사 : 애플리케이션 여러 로직에서 공통으로 관심이 있는 것
- 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
필터 적용 시 필터 호출 후 서블릿이 호출된다
필터에 URL 패턴을 적용할 수 있다 ex) /* : 모든 요청에 필터 적용
※ 스프링 사용 시, 서블릿 = 스프링의 디스패처 서블릿
- 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자
- 체인
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
필터는 체인으로 구성돼 중간에 자유롭게 추가할 수 있다
- 인터페이스
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException{}
public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
init() : 필터 초기화 메서드 / 서블릿 컨테이너가 생성될 때 호출됨
doFilter() : 요청이 올 때마다 해당 메서드가 호출됨 / 필터의 로직 구현
destroy() : 필터 종료 메서드 / 서블릿 컨테이너가 종료될 때 호출됨
서블릿 필터 - 요청 로그
- 로그 필터
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(); // 요청 구분 위해 UUID 생성
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");
}
}
- 필터 설정
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter()); // 등록할 필터 지정
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*"); // 모든 URL에 적용
return filterRegistrationBean;
}
}
서블릿 필터 - 인증 체크
- 로그인 체크 필터
public class LoginCheckFilter implements Filter {
// 해당 URL은 필터 통과
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); // 로그인하면 기존 URI로 돌아갈 수 있도록
return; // 필터 진행 끝
}
}
chain.doFilter(request, response);
} catch (Exception e){
throw e;
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
private boolean isLoginCheckPath(String requestURI){
return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
}
}
- WebConfig
@Bean
public FilterRegistrationBean loginCheckFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*"); // 모든 URL에 적용
return filterRegistrationBean;
}
- LoginController
@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";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL; //// 로그인 후, redirectURL 파라미터 있으면 해당 페이지로 이동, 없으면 홈으로
}
스프링 인터셉터
웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술 (스프링 MVC가 제공)
cf) 서블릿 필터 => 서블릿이 제공하는 기술
- 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
인터셉터는 디스패처 서블릿 - 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다
스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장한다 (스프링 MVC의 시작점이 디스패처 서블릿)
서블릿보다 훨씬 정밀하게 URL 패턴을 설정할 수 있다
- 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X) // 비 로그인 사용자
- 체인
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
- 인터페이스
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {}
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() 하나만 제공
request , response만 제공
인터셉터 :
컨트롤러 호출 전 ( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion ) 3가지 메소드 제공
어떤 컨트롤러( handler )가 호출되는지, 어떤 modelAndView 가 반환되는지 알 수 있음 (호출, 응답 정보)
- 호출 흐름
preHandle : 컨트롤러 호출 전에 호출 (정확히는 핸들러 어댑터 호출 전에 호출)
응답값이 tru면 다음으로 진행, false면 진행 X
postHandle : 컨트롤러 호출 후 호출 (정확히는 핸들러 어댑터 호출 후에 호출)
afterCompletion : 뷰가 렌더링된 후 호출
- 예외 상황
postHandle : 컨트롤러에서 예외가 발생하면 postHandle 호출 X
afterCompletion : 항상 호출 => 예외를 파라미터로 받아 어떤 예외가 발생했는지 로그로 출력 가능
스프링 인터셉터 - 요청 로그
- 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(); // 요청 로그 구분 위해 uuid 세팅
request.setAttribute(LOG_ID, uuid); // afterCompletion 에서도 사용 위해 uuid를 request에 담음
// @Controller, @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("REQUEST {} {} {}" , logId, requestURI, handler);
if (ex != null){
log.error("afterCompletion error!", ex);
}
}
}
- Webconfig - 인터셉터 등록
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()) // 인터셉터 등록
.order(1) // 호출 순서
.addPathPatterns("/**") // 인터셉터 적용할 url 패턴
.excludePathPatterns("/css/**", "/*.ico", "/error"); // 적용 제외할 패턴
필터, 인터셉터가 중복으로 처리되지 않도록 logFilter 메소드의 @Bean를 주석 처리해야 한다.
※ PathPattern
? : 한 문자 일치
* : 경로(/) 안에서 0개 이상의 문자 일치
** : 경로 끝까지 0개 이상의 경로(/) 일치
{spring} : 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} : matches the regexp [a-z]+ as a path variable named "spring"
{spring:[a-z]+} : regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} : 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처
스프링 인터셉터 - 인증 체크
- 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);
return false;
}
return true;
}
}
서블릿 필터에 비해 비교적 간단하다.
순서, 세밀한 설정은 WebConfig에서 한다.
- WebConfig
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error"); // interceptor 적용 제외
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/", "/members/add", "/login", "/logout",
"/css/**", "/*.ico", "/error");
}
ArgumentResolver 활용
Argument Resolver : 컨트롤러에 요청이 들어왔을 때, 특정 요청 값을 추출, 가공해 원하는 객체로 만듦
- HomeController
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) { ////
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
- Login 어노테이션
@Target(ElementType.PARAMETER) // 파라미터에 해당 어노테이션 사용
@Retention(RetentionPolicy.RUNTIME) // 리플렉션 등 활용할 수 있도록 런타임까지 어노테이션 정보 보관
public @interface Login {
}
- Argument Resolver
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
// parameter 정보 확인
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class); // @Login 추가돼있는지
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType()); // 파라미터 타입이 Member인지
return hasLoginAnnotation && hasMemberType; // true면 resolveArgument 메소드 실행
}
@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); // member 또는 null 반환
}
}
- WebConfig - Argument Resolver 등록
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
'Programming > 스프링' 카테고리의 다른 글
[김영한 스프링 MVC 2] 예외 처리와 오류 페이지 (0) | 2024.05.11 |
---|---|
logback-spring.xml로 로그 설정하기 (+ log rotate) (0) | 2024.04.04 |
[김영한 스프링 MVC 2] 로그인 처리1 - 쿠키, 세션 (0) | 2024.01.19 |
[김영한 스프링 MVC 2] 검증2 - Bean Validation (0) | 2024.01.16 |
[김영한 스프링 MVC 2] 검증1 - Validation (0) | 2023.12.03 |