로그인 처리 - 필터, 인터셉터
- 만약 로그인을 한 사용자에게만 어떤 기능(상품 관리, 마이페이지 등...)을 제공한다고 할 때 해당 URL을 알면 로그인을 하지 않아도 해당 화면에 들어갈 수 있다.
- 그렇기에 로그인 사용자에게만 제공하는 기능 관련 로직에서 로그인 여부를 체크해야 한다.
이때 1~2개의 기능이 아닌 수 많은 기능들에서 로그인 여부를 체크해야 하는데 애플리케이션 여러 로직에서 공통으로 관심있는 것을 공통 관심사(cross-cutting concern)라고 한다. - ex) 상품 등록, 수정, 조회 등 여러 로직에서 공통으로 인증에 대한 관심을 가지고 있다.
- 그렇기에 로그인 사용자에게만 제공하는 기능 관련 로직에서 로그인 여부를 체크해야 한다.
- 공통 관심사는 스프링 AOP로도 해결할 수 있지만 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
- 웹과 관련된 공통 관심사를 처리할 때는 HTTP 헤더나 URL 정보가 필요한데 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공하기 때문에
서블릿 필터
- 필터는 서블릿이 지원하는 수문장 역할을 한다.
- 필터 특성
- 필터 흐름
- 필터를 적용하면 필터가 호출된 다음 서블릿이 호출된다.
- 특정 URL 패턴에도 적용할 수 있다.
- ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
- 필터 제한
- 필터에서 적절하지 않은 요청이라 판단하면 제한할 수 있다.
- 로그인 사용자 ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
비 로그인 사용자 ex) HTTP 요청 -> WAS -> 필터 (적절하지 않은 요청이라 판단, 서블릿 호출 X)
- 필터 체인
- 필터는 체인으로 구성되기에 중간에 필터를 자유롭게 추가할 수 있다.
- ex) HTTP 요청 -> WAS -> 필터1(로그 남기는 필터) -> 필터2(로그인 여부 체크 필터) -> 서블릿 -> 컨트롤러
- 필터 흐름
- 여기서 말하는 서블릿은 스프링의 디스패처 서블릿으로 생각하면 된다.
//필터 인터페이스
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() : 필터 종료 메소드, 서블릿 컨테이너가 종료될 때 호출된다.
- javax.servlet의 Filter 인터페이스를 구현해야한다.
서블릿 필터 - 요청 로그
@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");
}
}
- 필터 인터페이스 구현
- HTTP 요청이 오면 doFilter()가 호출된다.
- ServletRequest는 HTTP 요청이 아닌 경우까지 고려한 것이기에 HttpServletRequest로 다운 캐스팅을 하면 된다.
- HTTP 요청을 구분하기 위해 요청당 임의의 uuid 생성
- uuid를 통해 요청, 응답 쌍을 확인할 수 있고 그로 인해 소요 시간등을 확인하고 성능 최적화를 할 수 있다.
- 가장 핵심 코드는 chain.doFilter() 이다.
- 다음 필터가 있으면 필터를 호출하고 없으면 서블릿을 호출한다.
- 이 로직이 없으면 다음 단계로 진행되지 않는다.
- Filter는 인터페이스지만 doFilter()를 제외한 메소드들이 default로 선언되어 있기에 모두 구현하지 않아도 된다.
doFilter() 만 구현해도 된다.
- HTTP 요청이 오면 doFilter()가 호출된다.
//필터 등록
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter());
filterFilterRegistrationBean.setOrder(1); //체인 순서
filterFilterRegistrationBean.addUrlPatterns("/*"); //URL 패턴 적용
return filterFilterRegistrationBean;
}
}
- 필터 등록 방법
- 스프링 부트를 사용하는 경우 FilterRegistrationBean을 사용해서 등록하면 된다.
- setFilter() : 등록할 필터를 지정한다.
- setOrder() : 필터는 체인으로 동작하기에 순서를 지정한다. (낮을수록 먼저 동작)
- addUrlPattern() : 필터를 적용할 URL 패턴을 지정한다. (/* = 전체 URL)
- @ServletComponentScan, @WebFilter(filtername, urlPattern)으로 필터 등록을 할 수 있지만 필터 순서가 조절이 되지 않는다. (FilterRegistrationBean 을 사용 권장)
- 스프링 부트를 사용하는 경우 FilterRegistrationBean을 사용해서 등록하면 된다.
- 실무에서 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();
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);
}
}
- 인증 필터를 적용하더라도 메인화면, 회원가입, 로그인, css 등은 접근할 수 있어야 하므로 화이트 리스트 경로로 등록하여 인증하지 않아도 되는 경로를 지정한다.
- httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
- 비로그인 / 미인증 사용자는 로그인 화면으로 다시 리다이렉트한다.
이때 로그인 이후 다시 홈 화면으로 이동하게 되면 원하는 경로를 다시 찾아가야하는 불편함이 있기에 로그인 화면으로 리다이렉트하면서 쿼리 파라미터로 현재 요청 경로를 보내준다. - 미인증 사용자가 로그인 성공 시 이전 요청 경로로 다시 이동하기 위해서는 해당 기능을 추가로 개발해야한다.
- 비로그인 / 미인증 사용자는 로그인 화면으로 다시 리다이렉트한다.
- return; 을 통해 필터를 더는 진행하지 않도록 한다. (이 부분이 중요)
- 다음 필터나 서블릿을 호출하지 않으며 redirect를 사용했기 때문에 redirect가 응답으로 적용되고 요청이 끝난다. (finally 구문은 return을 하더라도 무조건 수행된다.)
- 미인증 사용자는 다음으로 진행하지 않고 끝내는 장치이다.
//인증 필터 스프링 빈 등록
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
filterFilterRegistrationBean.setOrder(2); //체인 순서
filterFilterRegistrationBean.addUrlPatterns("/*"); //URL 패턴 적용
return filterFilterRegistrationBean;
}
- 인증 필터도 마찬가지로 스프링 빈으로 등록한다.
- URL 패턴은 이미 인증 필터 로직에서 화이트 리스트를 통해 걸러줬기에 모든 URL을 적용한다.
이 경우 새로운 기능이 개발되고 URL이 생겨도 코드를 수정하지 않아도 된다.
- URL 패턴은 이미 인증 필터 로직에서 화이트 리스트를 통해 걸러줬기에 모든 URL을 적용한다.
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
//로그인 실패 처리
...
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL;
}
- 미인증, 비로그인 사용자가 인증 필터에 의해서 /login에 쿼리 파라미터로 요청 경로를 포함해서 리다이렉트된 것을 처리하는 로직이 추가되었다.
- @RequestParam으로 쿼리 파라미터를 통해 전달받은 요청 경로를 로그인 성공 시 해당 경로로 다시 이동할 수 있도록 한다.
- dafaultValue = "/" 는 쿼리 파라미터가 없는 일반적인 로그인의 경우 메인 화면으로 이동하고 요청 경로를 파라미터로 받은 경우 해당 경로로 이동할 수 있도록 하는 장치이다.
스프링 인터셉터
- 스프링 인터셉터 : 웹과 관련된 공통 관심 사항을 처리하는 스프링 MVC에서 제공하는 기술이다.
- 스프링 MVC 구조에 특화된 필터 기능을 제공한다.
- 스프링 인터셉터 특성
- 인터셉터 흐름
- 인터셉터는 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다.
- 서블릿 URL 패턴보다 훨씬 정밀하게 설정할 수 있다.
- ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
- 인터셉터 제한
- 인터셉터에서 적절하지 않은 요청으로 판단하면 제한할 수 있다.
- 로그인 사용자 ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
비 로그인 사용자 ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 (적절하지 않은 요청이라 판단, 컨트롤러 호출 X)
- 인터셉터 체인
- 인터셉터는 체인으로 구성되며 중간에 자유롭게 추가할 수 있다.
- ex) 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 {}
}
- 인터셉터 인터페이스 메소드
- preHandle() : 컨트롤러 호출 전에 호출된다.
- postHandle() : 컨트롤러 호출 후 호출된다.
- afterCompletion() : 요청 완료 이후 호출된다.
- 서블릿 필터는 단순히 request, response만 제공했지만 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보를 받을 수 있으며 어떤 modelAndView가 반환되는지 응답 정보를 받을 수 있다.


- 스프링 인터셉터 정상 흐름
- preHandle() : 핸들러 어댑터 호출 전에 호출된다.
- 반환 값이 true이면 다음으로 진행한다.
- false이면 더이상 진행하지 않는다. 나머지 인터셉터 뿐만 아니라 핸들러 어댑터도 호출하지 않는다.
- postHandle() : 핸들러 어댑터 호출 후에 호출된다.
- afterCompletion() : 뷰가 렌더링 된 이후 호출된다.
- preHandle() : 핸들러 어댑터 호출 전에 호출된다.
- 스프링 인터셉터 예외 흐름
- preHandle() : 컨트롤러 호출 전에 호출된다.
- postHandle() : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.
- afterCompletion() : afterCompletion 은 항상 호출된다.
- 예외가 발생해도 호출되며 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다
- 예외와 무관하게 공통 처리를 하려면 afterCompletion()을 사용해야 한다.
- 스프링 MVC 구조를 사용하고 필터를 특별히 사용해야하는 경우가 아니라면 인터셉터를 사용하는 것이 더 편리하다.
스프링 인터셉터 - 요청 로그
@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 uuid = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);
if (ex != null) {
//오류는 {} 중괄호 사용하지 않아도 된다.
log.error("afterCompletion error!");
}
}
}
- 인터셉터를 구현하기 위해서는 HandlerInterceptor 인터페이스를 구현해야 한다.
- 필터와 달리 HttpServletRequest / Response를 제공하기에 편리하다.
- 서블릿 필터에서는 uuid를 지역 변수로 사용할 수 있었지만 인터셉터는 호출 시점이 완전히 분리되어 있다.
- preHandle()에서 사용한 uuid를 afterCompletion()에서 사용하기 위해서 static 필드로 만들면 안된다.
- 인터셉터는 싱글톤이 보장되기 때문에 멤버 변수 static을 사용하면 예상과 다른 결과가 나올 수 있다.
- request.setAttribute()로 값을 넘겨주고 request.getAttribute()로 값을 전달받을 수 있다.
- preHandle()에서 사용한 uuid를 afterCompletion()에서 사용하기 위해서 static 필드로 만들면 안된다.
- 핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다.
- 스프링에서 일반적으로 사용하는 @Controller, @RequestMapping을 활용한 핸들러 매핑을 사용하면 핸들러 정보로 HandlerMethod가 넘어온다.
- @Controller가 아닌 /resources/static과 같은 정적 리소스를 호출되면 ResourceHttpRequestHandler가 핸들러 정보로 넘어오게 된다.
- Slf4j의 log.error()로 오류를 로깅할 때는 log.info()와 같이 {} 중괄호를 사용하지 않아도 된다.
//인터셉터 스프링 빈 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "error");
}
}
- 인터셉터를 스프링 빈으로 등록할 때는 WebMvcConfigurer을 구현한 후 addInterceptors() 메소드를 구현하면 된다.
- 체인 방식으로 작성한다.
- excludePathPattern(): 인터셉터에서 제외할 패턴을 지정한다.
- 여러 인터셉터를 등록할 때도 addInterceptors() 메소드 안에 registry.~ 로 계속 등록할 수 있다.
- 스프링이 제공하는 URL 경로는 서블릿이 제공하는 URL 경로와 완전히 다르고 세밀하게 설정할 수 있다.
- 사용할 때는 별도로 확인해볼 것
스프링 인터셉터 - 인증 체크
//인터셉터 인증 체크 로직
@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;
}
}
- 인터셉터 인터페이스의 메소드는 모두 default로 선언되어 있기에 필요한 메소드만 구현할 수 있다.
- 로그인 인증 체크 로직은 컨트롤러 호출 전에만 호출하면 되기에 preHandle()만 구현하면 된다.
- 인터셉터의 코드는 서블릿 필터보다 훨씬 간결하다.
- 인증 체크 로직에 화이트 리스트 코드도 별도로 작성하지 않고 인터셉터 등록시 excludePathPattern()으로 등록할 수 있다.
- 미인증 사용자의 경우 마찬가지로 redirect 하며 return false를 해야 다음 로직으로 넘어가지 않는다.
ArgumentResolver 활용
- ArgumentResolver를 구현함으로 Argument를 처리하는 과정을 직접 처리할 수 있다.
//컨트롤러 홈 화면 로직
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//세션에 회원 데이터가 없으면
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
- 세션 값을 파라미터 @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember -> @Login Member loginMember로 변경
- @Login은 직접 만든 어노테이션이다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
- @Target() : 어노테이션의 적용되는 범위를 설정한다.
- @Retention() : 어노테이션의 라이프 사이클을 설정한다.
//ArgumentResolver 구현
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
//파라미터에 @Login 어노테이션이 있는지 확인
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
//파라미터의 타입이 Member와 일치하는지 확인
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//supportsParameter() 메소드가 true이면 실행
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
- ArgumentResolver를 구현하기 위해 HandlerMethodArgumentResolver 인터페이스를 구현한다
- supportsParameter() : Resolver가 해당 파라미터를 지원하는지 판단한다.
- 여기서는 @Login 어노테이션이 있고 파라미터 타입이 Member와 일치하면 Resolver가 사용된다.
- resolveArgument() :컨트롤러 호출 직전에 호출되어서 필요한 파라미터 정보를 생성해준다.
- 여기서는 세션에 있는 로그인 회원 정보를 찾아서 컨트롤러 메소드에 파라미터로 전달해준다.
- supportsParameter() : Resolver가 해당 파라미터를 지원하는지 판단한다.
//ArgumentResolver 스프링 빈 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
- ArgumentResolver 구현체 스프링 빈으로 등록
- ArgumentResolver를 활용하면 공통 작업이 필요할 때 컨트롤러를 더욱 편리하게 사용할 수 있다.
출처 : [인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 | 김영한 - 인프런
김영한 | 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습
www.inflearn.com
'Spring > [인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술]' 카테고리의 다른 글
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] API 예외 처리 (2) | 2024.11.06 |
---|---|
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 예외 처리와 오류 페이지 (2) | 2024.10.18 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 로그인 처리 - 쿠키, 세션 (4) | 2024.10.15 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 검증 - Bean Validation (1) | 2024.10.14 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 검증 - Validation (4) | 2024.10.14 |
로그인 처리 - 필터, 인터셉터
- 만약 로그인을 한 사용자에게만 어떤 기능(상품 관리, 마이페이지 등...)을 제공한다고 할 때 해당 URL을 알면 로그인을 하지 않아도 해당 화면에 들어갈 수 있다.
- 그렇기에 로그인 사용자에게만 제공하는 기능 관련 로직에서 로그인 여부를 체크해야 한다.
이때 1~2개의 기능이 아닌 수 많은 기능들에서 로그인 여부를 체크해야 하는데 애플리케이션 여러 로직에서 공통으로 관심있는 것을 공통 관심사(cross-cutting concern)라고 한다. - ex) 상품 등록, 수정, 조회 등 여러 로직에서 공통으로 인증에 대한 관심을 가지고 있다.
- 그렇기에 로그인 사용자에게만 제공하는 기능 관련 로직에서 로그인 여부를 체크해야 한다.
- 공통 관심사는 스프링 AOP로도 해결할 수 있지만 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
- 웹과 관련된 공통 관심사를 처리할 때는 HTTP 헤더나 URL 정보가 필요한데 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공하기 때문에
서블릿 필터
- 필터는 서블릿이 지원하는 수문장 역할을 한다.
- 필터 특성
- 필터 흐름
- 필터를 적용하면 필터가 호출된 다음 서블릿이 호출된다.
- 특정 URL 패턴에도 적용할 수 있다.
- ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
- 필터 제한
- 필터에서 적절하지 않은 요청이라 판단하면 제한할 수 있다.
- 로그인 사용자 ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
비 로그인 사용자 ex) HTTP 요청 -> WAS -> 필터 (적절하지 않은 요청이라 판단, 서블릿 호출 X)
- 필터 체인
- 필터는 체인으로 구성되기에 중간에 필터를 자유롭게 추가할 수 있다.
- ex) HTTP 요청 -> WAS -> 필터1(로그 남기는 필터) -> 필터2(로그인 여부 체크 필터) -> 서블릿 -> 컨트롤러
- 필터 흐름
- 여기서 말하는 서블릿은 스프링의 디스패처 서블릿으로 생각하면 된다.
//필터 인터페이스
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() : 필터 종료 메소드, 서블릿 컨테이너가 종료될 때 호출된다.
- javax.servlet의 Filter 인터페이스를 구현해야한다.
서블릿 필터 - 요청 로그
@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");
}
}
- 필터 인터페이스 구현
- HTTP 요청이 오면 doFilter()가 호출된다.
- ServletRequest는 HTTP 요청이 아닌 경우까지 고려한 것이기에 HttpServletRequest로 다운 캐스팅을 하면 된다.
- HTTP 요청을 구분하기 위해 요청당 임의의 uuid 생성
- uuid를 통해 요청, 응답 쌍을 확인할 수 있고 그로 인해 소요 시간등을 확인하고 성능 최적화를 할 수 있다.
- 가장 핵심 코드는 chain.doFilter() 이다.
- 다음 필터가 있으면 필터를 호출하고 없으면 서블릿을 호출한다.
- 이 로직이 없으면 다음 단계로 진행되지 않는다.
- Filter는 인터페이스지만 doFilter()를 제외한 메소드들이 default로 선언되어 있기에 모두 구현하지 않아도 된다.
doFilter() 만 구현해도 된다.
- HTTP 요청이 오면 doFilter()가 호출된다.
//필터 등록
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter());
filterFilterRegistrationBean.setOrder(1); //체인 순서
filterFilterRegistrationBean.addUrlPatterns("/*"); //URL 패턴 적용
return filterFilterRegistrationBean;
}
}
- 필터 등록 방법
- 스프링 부트를 사용하는 경우 FilterRegistrationBean을 사용해서 등록하면 된다.
- setFilter() : 등록할 필터를 지정한다.
- setOrder() : 필터는 체인으로 동작하기에 순서를 지정한다. (낮을수록 먼저 동작)
- addUrlPattern() : 필터를 적용할 URL 패턴을 지정한다. (/* = 전체 URL)
- @ServletComponentScan, @WebFilter(filtername, urlPattern)으로 필터 등록을 할 수 있지만 필터 순서가 조절이 되지 않는다. (FilterRegistrationBean 을 사용 권장)
- 스프링 부트를 사용하는 경우 FilterRegistrationBean을 사용해서 등록하면 된다.
- 실무에서 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();
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);
}
}
- 인증 필터를 적용하더라도 메인화면, 회원가입, 로그인, css 등은 접근할 수 있어야 하므로 화이트 리스트 경로로 등록하여 인증하지 않아도 되는 경로를 지정한다.
- httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
- 비로그인 / 미인증 사용자는 로그인 화면으로 다시 리다이렉트한다.
이때 로그인 이후 다시 홈 화면으로 이동하게 되면 원하는 경로를 다시 찾아가야하는 불편함이 있기에 로그인 화면으로 리다이렉트하면서 쿼리 파라미터로 현재 요청 경로를 보내준다. - 미인증 사용자가 로그인 성공 시 이전 요청 경로로 다시 이동하기 위해서는 해당 기능을 추가로 개발해야한다.
- 비로그인 / 미인증 사용자는 로그인 화면으로 다시 리다이렉트한다.
- return; 을 통해 필터를 더는 진행하지 않도록 한다. (이 부분이 중요)
- 다음 필터나 서블릿을 호출하지 않으며 redirect를 사용했기 때문에 redirect가 응답으로 적용되고 요청이 끝난다. (finally 구문은 return을 하더라도 무조건 수행된다.)
- 미인증 사용자는 다음으로 진행하지 않고 끝내는 장치이다.
//인증 필터 스프링 빈 등록
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
filterFilterRegistrationBean.setOrder(2); //체인 순서
filterFilterRegistrationBean.addUrlPatterns("/*"); //URL 패턴 적용
return filterFilterRegistrationBean;
}
- 인증 필터도 마찬가지로 스프링 빈으로 등록한다.
- URL 패턴은 이미 인증 필터 로직에서 화이트 리스트를 통해 걸러줬기에 모든 URL을 적용한다.
이 경우 새로운 기능이 개발되고 URL이 생겨도 코드를 수정하지 않아도 된다.
- URL 패턴은 이미 인증 필터 로직에서 화이트 리스트를 통해 걸러줬기에 모든 URL을 적용한다.
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
//로그인 실패 처리
...
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL;
}
- 미인증, 비로그인 사용자가 인증 필터에 의해서 /login에 쿼리 파라미터로 요청 경로를 포함해서 리다이렉트된 것을 처리하는 로직이 추가되었다.
- @RequestParam으로 쿼리 파라미터를 통해 전달받은 요청 경로를 로그인 성공 시 해당 경로로 다시 이동할 수 있도록 한다.
- dafaultValue = "/" 는 쿼리 파라미터가 없는 일반적인 로그인의 경우 메인 화면으로 이동하고 요청 경로를 파라미터로 받은 경우 해당 경로로 이동할 수 있도록 하는 장치이다.
스프링 인터셉터
- 스프링 인터셉터 : 웹과 관련된 공통 관심 사항을 처리하는 스프링 MVC에서 제공하는 기술이다.
- 스프링 MVC 구조에 특화된 필터 기능을 제공한다.
- 스프링 인터셉터 특성
- 인터셉터 흐름
- 인터셉터는 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출된다.
- 서블릿 URL 패턴보다 훨씬 정밀하게 설정할 수 있다.
- ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
- 인터셉터 제한
- 인터셉터에서 적절하지 않은 요청으로 판단하면 제한할 수 있다.
- 로그인 사용자 ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
비 로그인 사용자 ex) HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 (적절하지 않은 요청이라 판단, 컨트롤러 호출 X)
- 인터셉터 체인
- 인터셉터는 체인으로 구성되며 중간에 자유롭게 추가할 수 있다.
- ex) 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 {}
}
- 인터셉터 인터페이스 메소드
- preHandle() : 컨트롤러 호출 전에 호출된다.
- postHandle() : 컨트롤러 호출 후 호출된다.
- afterCompletion() : 요청 완료 이후 호출된다.
- 서블릿 필터는 단순히 request, response만 제공했지만 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보를 받을 수 있으며 어떤 modelAndView가 반환되는지 응답 정보를 받을 수 있다.


- 스프링 인터셉터 정상 흐름
- preHandle() : 핸들러 어댑터 호출 전에 호출된다.
- 반환 값이 true이면 다음으로 진행한다.
- false이면 더이상 진행하지 않는다. 나머지 인터셉터 뿐만 아니라 핸들러 어댑터도 호출하지 않는다.
- postHandle() : 핸들러 어댑터 호출 후에 호출된다.
- afterCompletion() : 뷰가 렌더링 된 이후 호출된다.
- preHandle() : 핸들러 어댑터 호출 전에 호출된다.
- 스프링 인터셉터 예외 흐름
- preHandle() : 컨트롤러 호출 전에 호출된다.
- postHandle() : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.
- afterCompletion() : afterCompletion 은 항상 호출된다.
- 예외가 발생해도 호출되며 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다
- 예외와 무관하게 공통 처리를 하려면 afterCompletion()을 사용해야 한다.
- 스프링 MVC 구조를 사용하고 필터를 특별히 사용해야하는 경우가 아니라면 인터셉터를 사용하는 것이 더 편리하다.
스프링 인터셉터 - 요청 로그
@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 uuid = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);
if (ex != null) {
//오류는 {} 중괄호 사용하지 않아도 된다.
log.error("afterCompletion error!");
}
}
}
- 인터셉터를 구현하기 위해서는 HandlerInterceptor 인터페이스를 구현해야 한다.
- 필터와 달리 HttpServletRequest / Response를 제공하기에 편리하다.
- 서블릿 필터에서는 uuid를 지역 변수로 사용할 수 있었지만 인터셉터는 호출 시점이 완전히 분리되어 있다.
- preHandle()에서 사용한 uuid를 afterCompletion()에서 사용하기 위해서 static 필드로 만들면 안된다.
- 인터셉터는 싱글톤이 보장되기 때문에 멤버 변수 static을 사용하면 예상과 다른 결과가 나올 수 있다.
- request.setAttribute()로 값을 넘겨주고 request.getAttribute()로 값을 전달받을 수 있다.
- preHandle()에서 사용한 uuid를 afterCompletion()에서 사용하기 위해서 static 필드로 만들면 안된다.
- 핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다.
- 스프링에서 일반적으로 사용하는 @Controller, @RequestMapping을 활용한 핸들러 매핑을 사용하면 핸들러 정보로 HandlerMethod가 넘어온다.
- @Controller가 아닌 /resources/static과 같은 정적 리소스를 호출되면 ResourceHttpRequestHandler가 핸들러 정보로 넘어오게 된다.
- Slf4j의 log.error()로 오류를 로깅할 때는 log.info()와 같이 {} 중괄호를 사용하지 않아도 된다.
//인터셉터 스프링 빈 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "error");
}
}
- 인터셉터를 스프링 빈으로 등록할 때는 WebMvcConfigurer을 구현한 후 addInterceptors() 메소드를 구현하면 된다.
- 체인 방식으로 작성한다.
- excludePathPattern(): 인터셉터에서 제외할 패턴을 지정한다.
- 여러 인터셉터를 등록할 때도 addInterceptors() 메소드 안에 registry.~ 로 계속 등록할 수 있다.
- 스프링이 제공하는 URL 경로는 서블릿이 제공하는 URL 경로와 완전히 다르고 세밀하게 설정할 수 있다.
- 사용할 때는 별도로 확인해볼 것
스프링 인터셉터 - 인증 체크
//인터셉터 인증 체크 로직
@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;
}
}
- 인터셉터 인터페이스의 메소드는 모두 default로 선언되어 있기에 필요한 메소드만 구현할 수 있다.
- 로그인 인증 체크 로직은 컨트롤러 호출 전에만 호출하면 되기에 preHandle()만 구현하면 된다.
- 인터셉터의 코드는 서블릿 필터보다 훨씬 간결하다.
- 인증 체크 로직에 화이트 리스트 코드도 별도로 작성하지 않고 인터셉터 등록시 excludePathPattern()으로 등록할 수 있다.
- 미인증 사용자의 경우 마찬가지로 redirect 하며 return false를 해야 다음 로직으로 넘어가지 않는다.
ArgumentResolver 활용
- ArgumentResolver를 구현함으로 Argument를 처리하는 과정을 직접 처리할 수 있다.
//컨트롤러 홈 화면 로직
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//세션에 회원 데이터가 없으면
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
- 세션 값을 파라미터 @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember -> @Login Member loginMember로 변경
- @Login은 직접 만든 어노테이션이다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
- @Target() : 어노테이션의 적용되는 범위를 설정한다.
- @Retention() : 어노테이션의 라이프 사이클을 설정한다.
//ArgumentResolver 구현
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
//파라미터에 @Login 어노테이션이 있는지 확인
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
//파라미터의 타입이 Member와 일치하는지 확인
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
//supportsParameter() 메소드가 true이면 실행
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
- ArgumentResolver를 구현하기 위해 HandlerMethodArgumentResolver 인터페이스를 구현한다
- supportsParameter() : Resolver가 해당 파라미터를 지원하는지 판단한다.
- 여기서는 @Login 어노테이션이 있고 파라미터 타입이 Member와 일치하면 Resolver가 사용된다.
- resolveArgument() :컨트롤러 호출 직전에 호출되어서 필요한 파라미터 정보를 생성해준다.
- 여기서는 세션에 있는 로그인 회원 정보를 찾아서 컨트롤러 메소드에 파라미터로 전달해준다.
- supportsParameter() : Resolver가 해당 파라미터를 지원하는지 판단한다.
//ArgumentResolver 스프링 빈 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
- ArgumentResolver 구현체 스프링 빈으로 등록
- ArgumentResolver를 활용하면 공통 작업이 필요할 때 컨트롤러를 더욱 편리하게 사용할 수 있다.
출처 : [인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술]
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 | 김영한 - 인프런
김영한 | 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습
www.inflearn.com
'Spring > [인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술]' 카테고리의 다른 글
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] API 예외 처리 (2) | 2024.11.06 |
---|---|
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 예외 처리와 오류 페이지 (2) | 2024.10.18 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 로그인 처리 - 쿠키, 세션 (4) | 2024.10.15 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 검증 - Bean Validation (1) | 2024.10.14 |
[인프런 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술] 검증 - Validation (4) | 2024.10.14 |