728x90
공통 관심 사항
- 로그인하지 않은 사용자가 URL을 직접 입력하면 상품 관리 화면에 들어갈 수 있는 문제가 있다.
- 상품 관리 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만, 등록, 수정, 삭제, 조회 등등 상품관리의 모든 컨트롤러 로직에 공통으로 로그인 여부를 확인해야 한다.
- 더 큰 문제는 향후 로그인과 관련된 로직이 변경될 때 작성한 모든 로직을 수정해야 할 수 있다.
- 이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만, 웹과 관련된 공통 관심사는 지금부터 설명할 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다
- 웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공한다.
서블릿 필터
- 필터 흐름
- HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
- 필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출된다.
- 그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다. 참
- 고로 필터는 특정 URL 패턴에 적용할 수 있다. /*이라고 하면 모든 요청에 필터가 적용된다.
- 필터 제한
- 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(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다
요청 로그
Filter 만들기
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void destroy() {
log.info("log filter destroy");
}
@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);
}
}
}
- javax.servlet의 Filter 인터페이스를 구현한다.
- doFilter
- Http 요청이 들어오게 되면 doFilter가 호출된다.
- 파라미터로 받는 ServletRequest는 Http 요청이 아닌 경우까지 고려해서 만들었기 때문에 다운 캐스팅을 해서 HttpServletRequest 기능들의 사용하면 된다.
- chain.doFilter를 통해 다음 필터를 호출한다.
- 필터가 없으면 서블릿을 호출한다.
WebConfig
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter());
filterFilterRegistrationBean.setOrder(1);
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
}
- SpringBoot를 사용하면 Filter를 등록할때 FilterRegustrationBean을 사용해서 등록하면 된다.
- setFilter(new LogFilter()) : 등록할 필터를 지정한다.
- setOrder(1) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을수록 먼저 동작한다.
- addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다
- 참고
- @ServletComponentScan, @WebFilter 로 필터 등록이 가능하지만 순서 조절이 안된다.(FilterRegustrationBean을 사용하자)
- 실무에서 HTTP 요청시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc로 검색해보자.
인증 체크
LoginCheckFilter
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
//init, destroy 이는 default라 구현 안해도 된다.
@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);
}
}
/**
* 화이트 list의 경우 인증 체크 X
*/
private boolean isLoginCheckPath(String requestURI){
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
- whiteList
- 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 리소스에는 접근할 수 있어야 한다.
- 이렇게 화이트 리스트 경로는 인증과 무관하게 항상 허용한다.
- 화이트 리스트를 제외한 나머지 모든 경로에는 인증 체크 로직을 적용한다.
- httpResponse.sendRedirect()
- 미인증 사용자는 로그인 화면으로 리다이렉트 한다. 그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾아가야 하는 불편함이 있다.
- 예를 들어서 상품 관리 화면을 보려고 들어갔다가 로그인 화면으로 이동하면, 로그인 이후에 다시 상품 관리 화면으로 들어가는 것이 좋다
- 이러한 기능을 위해 현재 요청한 경로인 requestURI를 /login에 쿼리 파라미터로 함께 전달한다
- return
- 필터를 더는 진행하지 않는다. 이후 필터는 물론 서블릿, 컨트롤러가 더는 호출되지 않는다.
- 앞서 redirect 를 사용했기 때문에 redirect 가 응답으로 적용되고 요청이 끝난다.
Bean 등록
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
filterFilterRegistrationBean.setOrder(2);
filterFilterRegistrationBean
.addUrlPatterns("/*"); //미래에 새로운 패턴이 생기더라도 적용시키기 위해서 whiteList에 있는 것들만 제외한 모든 요청을 실행했다
return filterFilterRegistrationBean;
}
loginControllerV4
@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(true);
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL;
}
- 더 이상 / 가 아닌 파라미터 요청으로 들어온 곳으로 Redirect 한다.
- 참고
- 필터에는 다음에 설명할 스프링 인터셉터는 제공하지 않는, 아주 강력한 기능이 있는데 chain.doFilter(request, response); 를 호출해서 다음 필터 또는 서블릿을 호출할 때 request , response를 다른 객체로 바꿀 수 있다.
- ServletRequest , ServletResponse를 구현한 다른 객체를 만들어서 넘기면 해당 객체가 다음 필터 또는 서블릿에서 사용된다. 잘 사용하는 기능은 아니니 참고만 해두자
728x90
'스프링 MVC 2편(백엔드 웹 개발 활용 기술)' 카테고리의 다른 글
| Ch07. 로그인 처리(필터, 인터셉터) - ArgumentResolver 활용 (0) | 2022.03.18 |
|---|---|
| Ch07. 로그인 처리(필터, 인터셉터) - 스프링 인터셉터 (0) | 2022.03.18 |
| Ch06. 로그인 처리(쿠키, 세션) - 세션 정보와 타임아웃 설정 (0) | 2022.03.17 |
| Ch06. 로그인 처리(쿠키, 세션) - 로그인 처리하기 (0) | 2022.03.17 |
| Ch06. 로그인 처리(쿠키, 세션) - 쿠키와 보안 문제 (0) | 2022.03.17 |