티스토리 뷰
Servlet Filter
필터는 서블릿이 지원하는 문지기와 같다.
만약 로그인한 회원만 게시판의 글 쓰기, 수정, 삭제 기능을 사용할 수 있다면, 해당 로직마다 로그인을 확인하는 기능을 추가해야 하고, 이 기능이 수정되었다면, 일일이 다 찾아서 수정도 해야 한다.
이러한 공통 관심사는 스프링의 AOP로 해결할 수 있지만, 웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
필터의 흐름은 다음과 같다.
HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 컨트롤러
필터를 적용하면 필터가 호출 된 다음에 디스패처 서블릿이 호출된다. 그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다. /*
이라고 하면 모든 요청에 필터가 적용된다.
아래는 필터로 제한을 적용했을 때, 흐름이다.
HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 컨트롤러 // 로그인한 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //미인증 사용자
필터 체인을 이용하여 여러 개의 필터를 사용할 수도 있다.(중간에 넣고, 빼는 것이 수월)
HTTP 요청 ->WAS-> 필터1-> 필터2-> 필터3-> 디스패처 서블릿 -> 컨트롤러
filter를 이용한 로그인 확인 기능 개발
filter를 사용하려면 filter 인터페이스를 구현해야 한다.
filter 인터페이스가 가지고 있는 메서드는 아래와 같다.
- init : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.(default 메서드이기 때문에, 구현하지 않아도 됨.)
- doFilter : 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
- destroy : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다. (default 메서드이기 때문에, 구현하지 않아도 됨.)
LogFilter 클래스
package com.login.web.filter;
import ...
@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(); // HTTP 요청을 구분하기 위한 요청당 임의의 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");
}
}
- doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
- HTTP 요청이 오면
doFilter
가 호출된다. ServletRequest request
는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다.- HTTP를 사용하면
HttpServletRequest httpRequest = (HttpServletRequest) request;
와 같이
다운 캐스팅 하면 된다.
- HTTP 요청이 오면
- chain.doFilter(request, response)
중요
- 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다
LoginCheckFilte 클래스
package com.login.web.filter;
import ...
@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 (isEmpty(session) || isEmpty(session.getAttribute(SessionConst.LOGIN_MEMBER))) {
log.info("미인증 사용자 요청 {}", requestURI);
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);
}
}
- whitelist = {"/", "/members/add", "/login", "/logout","/css/*"};
- 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, css 같은 리소스에는 접근할 수 있어야 한다.
- 이렇게 화이트 리스트 경로는 인증과 무관하게 항상 허용한다.
- 화이트 리스트를 제외한 나머지 모든 경로에는 인증 체크 로직을 적용한다.
- isLoginCheckPath(requestURI)
- 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용한다.
- httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
- 미인증 사용자는 로그인 화면으로 리다이렉트 한다.
- 로그인 후, 요청 페이지로 redirect 시키기 위해
/login
에 쿼리 파라미터로 함께 전달한다.
- return;
중요
- 필터를 더는 진행하지 않는다.
- 이후 필터는 물론 서블릿, 컨트롤러가 더는 호출되지 않는다.
- 앞서 redirect 를 사용했기 때문에 redirect 가 응답으로 적용되고 요청이 끝난다.
WebConifg 클래스
package com.login;
import ...
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LogFilter());
filterFilterRegistrationBean.setOrder(1);
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
filterFilterRegistrationBean.setOrder(2);
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
}
필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean
을 사용해서 등록하면 된다.
- setFilter()
- 등록할 필터를 지정한다.
- 위에서는
log
를 위한LogFilter
클래스와 로그인 확인을 위한LoginCheckFilter
를 등록하였다.
- setOrder()
- 필터는 체인으로 동작하기 때문에, 순서가 필요(낮을수록 먼저 동작)
LogFilter
가 먼저 적용되고,loginCheckFilter
가 적용된다.
- addUrlPatterns("/*")
- 필터를 적용할 URL 패턴을 지정한다.
/*
: 모든 요청에 필터 적용- 한번에 여러 패턴을 지정 가능
LoginController 클래스
@PostMapping("/login")
public String login(
@Valid @ModelAttribute("loginForm") LoginForm form,
BindingResult bindingResult,
HttpServletRequest request,
@RequestParam(defaultValue = "/") String redirectURL
) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (isEmpty(loginMember)) {
bindingResult.reject("loginFail", "아이디 또는 패스워드가 맞지 않습니다.");
return "login/loginForm";
}
HttpSession session = request.getSession();
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL;
}
- return "redirect:" + redirectURL;
LoginCheckFilter
에서 미인증 사용자가 로그인을 성공적으로 마친 뒤, 원래 페이지로redirect
시킨다.
로그인 확인과 같이 반복되는 작업을 서블릿 필터를 사용하여 처리한다면, SOLID의 단일책임원칙
을 위배하지 않고, 향후 로그인 관련된 정책이 변경되더라도 이 부분만 수정하면 된다.
'Spring' 카테고리의 다른 글
[Spring] Singleton, SingletonContainer (0) | 2022.01.22 |
---|---|
[Spring] Bean Validation (0) | 2022.01.21 |
[Spring] interceptor를 이용한 로그인 확인 기능 구현 (0) | 2022.01.21 |
[스프링] 의존성 주입(Dependency Injection) (0) | 2022.01.18 |
[Spring] 메시지 소스 & 국제화 (0) | 2022.01.17 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- 코테
- 스프링부트
- 노마드코더
- 코틀린
- 인프런
- Spring
- 정렬
- leetcode
- 리팩토링
- 문자열
- mysql 8.0
- 노마드
- Real MySQL
- 백준
- 알고리즘
- 자료구조
- Algorithm
- 릿코드
- 데이터베이스
- spring boot
- 구현
- 북클럽
- kotlin
- 스프링 부트
- 파이썬
- 김영한
- MySQL
- webflux
- 스프링
- 그리디
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함