2022. 7. 10. 13:30ㆍLanguage`/Spring
현재 Controller에 의해서 매핑되는 Page가 다음 종류가 있다고 하자
1. 로그인 하지 않고 접근 가능
2. 로그인 해야 접근 가능
>> 여기서 과연 "로그인 해야 접근 가능한 페이지"가 진짜 로그인을 해야만 접근 가능한지 확인해보자
- /members/1로 접근
결과로 알수있듯이 "로그인 후 전용 마이페이지"가 로그인을 하지 않은 사용자에게도 접근이 허용되고 있다
>> "URL"에 따라서 request를 필터링할 수 있게 도와주는 기능을 Servlet에서 제공해준다 : Servlet Filter
Servlet Filter
Servlet Filter는 Servlet이 제공해주는 "Request Filtering" 기능이고 일단 필터의 흐름부터 알아보자
Servlet Filter는 수문장 역할을 수행하고 서블릿으로 request가 들어가기전에 request에 대한 필터링을 해주는 역할이다
"Servlet Filter는 여러 필터를 Chaining해서 엮을 수 있다"
중간에 특정 Filter가 "이 Request는 적절하지 않은 요청입니다."라고 판단한다면 거기서 필터링을 끝내고 Exception을 던질 수도 있고 필터링을 거기서 중단하고 다시 검증하라고 요구할 수도 있다
>> 따라서 Servlet Filter들의 필터링이 완전히 끝나고 나서야 "DispatcherServlet"이 호출되고, 만약 필터링 중에 판단을 통해서 필터링을 중단하고 끊어버리면 DispatcherServlet은 호출되지 않는다
Filter <Interface>
Servlet Filter를 구현하려면 "Filter"라는 Interface를 구현해야 한다
Filter Interface는 총 3개의 메소드로 구현되어 있다
1. default void init
init은 필터를 초기화하는 메소드로, 서블릿 컨테이너가 생성될 때 호출된다
- default이므로 init은 무조건 구현해야 하는 메소드는 아니다
2. void doFilter
Filter의 핵심 메소드로써 Request가 들어올때마다 호출되는 메소드이다
doFilter내부에 핵심 비즈니스 필터 로직을 구현하면 된다
- default가 없기 때문에 doFilter는 반드시 구현해야 하는 메소드이다
3. default void destroy()
destory는 필터를 종료시키는 메소드로, 서블릿 컨테이너가 종료될 때 호출된다
>> 필터는 여러개의 필터로 Chaining할 수 있으므로 여러개의 필터를 구현해서 Chaining하는 것을 실습해보자
@Slf4j
public class ServletFilterV1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("----- Filter 1 >> init -----");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("----- Filter 1 >> doFilter [Start] -----");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String uuid = UUID.randomUUID().toString();
log.info("REQUEST = [{}][{}][{}]", uuid, httpServletRequest.getRequestURL(), httpServletRequest.getMethod());
chain.doFilter(request, response);
log.info("RESPONSE = [{}][{}][{}]", uuid, httpServletRequest.getRequestURL(), httpServletRequest.getMethod());
log.info("----- Filter 1 >> doFilter [End] -----");
}
@Override
public void destroy() {
log.info("----- Filter 1 >> destroy -----");
}
}
@Slf4j
public class ServletFilterV2 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("----- Filter 2 >> init -----");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("----- Filter 2 >> doFilter [Start] -----");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String uuid = UUID.randomUUID().toString();
log.info("REQUEST = [{}][{}][{}]", uuid, httpServletRequest.getRequestURL(), httpServletRequest.getMethod());
chain.doFilter(request, response);
log.info("RESPONSE = [{}][{}][{}]", uuid, httpServletRequest.getRequestURL(), httpServletRequest.getMethod());
log.info("----- Filter 2 >> doFilter [End] -----");
}
@Override
public void destroy() {
log.info("----- Filter 2 >> destroy -----");
}
}
@Slf4j
public class ServletFilterV3 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("----- Filter 3 >> init -----");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("----- Filter 3 >> doFilter [Start] -----");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String uuid = UUID.randomUUID().toString();
log.info("REQUEST = [{}][{}][{}]", uuid, httpServletRequest.getRequestURL(), httpServletRequest.getMethod());
chain.doFilter(request, response);
log.info("RESPONSE = [{}][{}][{}]", uuid, httpServletRequest.getRequestURL(), httpServletRequest.getMethod());
log.info("----- Filter 3 >> doFilter [End] -----");
}
@Override
public void destroy() {
log.info("----- Filter 3 >> destroy -----");
}
}
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean filterRegistrationBeanV1(){
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ServletFilterV1());
registrationBean.setOrder(1);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
@Bean
public FilterRegistrationBean filterRegistrationBeanV2(){
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ServletFilterV2());
registrationBean.setOrder(2);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
@Bean
public FilterRegistrationBean filterRegistrationBeanV3(){
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new ServletFilterV3());
registrationBean.setOrder(3);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
(1) init을 통한 필터 초기화
필터 초기화는 서블릿 컨테이너 생성시 딱 1번만 초기화 호출이 되고, 초기화 순서는 FilterConfig에서 지정해주었던 order와는 상관이 없다
(2) "/filter" Request에 의한 doFilter
WAS → Filter1 → Filter2 → .... Controller로 Request Flow가 이어지다가 Request에 대해서 Controller가 응답을 하면 다시 역으로 Controller → ... → Filter2 → Filter1 → WAS로 Flow가 이루어진다
(3) Servlet Container 종료
ServletFilter를 설계하고 만든 Filter를 Spring Boot에 적용시키려면 "FilterRegistrationBean"에 적용해서 Bean으로 만들면 된다
※ Example) 로그인 인증 필터 적용
@Slf4j
public class LoginFilter implements Filter {
private final static String [] whiteList = {
"/",
"/members/add",
"/login"
};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestURI = httpRequest.getRequestURI();
try{
log.info("--- 로그인 인증 필터 시작 ---");
if(!isValidRequest(requestURI)){
HttpSession session = httpRequest.getSession(false);
if(session == null || session.getAttribute(SessionConstField.MY_SESSION_NAME) == null){
// 로그인 페이지로 이동
log.info("미인증 사용자의 REQUEST = [{}][{}]", requestURI, httpRequest.getMethod());
httpResponse.sendRedirect("/login?redirectURI=" + requestURI);
return; // 더이상 필터를 진행시키지 않고 "종료"
}
}
log.info("인증된 사용자!!");
chain.doFilter(request, response);
} catch (Exception e){
throw e;
} finally {
log.info("--- 로그인 인증 필터 종료 ---");
}
}
private boolean isValidRequest(String requestURI){
return PatternMatchUtils.simpleMatch(whiteList, requestURI);
}
}
"/member/{id}"가 아닌 모든 URI는 로그인 없이 이용할 수 있다
따라서 이외 URI은 "whiteList"로 지정하였다
httpResponse.sendRedirect("/login?redirectURI=" + requestURI)
미인증 사용자의 경우 다시 로그인을 페이지로 이동하여야 한다
여기서 미인증 사용자가 처음 request했던 URI에 대해서 로그인만 성공하면 즉시 redirect시키도록 설계하였다
return
미인증 사용자의 경우 필터링 더 할 필요가 없기 때문에 "return;"을 통해서 즉시 필터링을 중지하고 서블릿도 호출하지 않는다
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new LoginFilter());
bean.setOrder(1);
bean.addUrlPatterns("/member/*");
return bean;
}
}
마찬가지로 "로그인 필터"를 구현하고나서 FilterRegistratonBean에 등록해주었다
addUrlPatterns의 경우 "이 필터를 어느 URI에 적용할까"라는 기능이고 우리는 "/member/{id}"에 적용을 할거니까 "/member/*"로 /member하위의 모든 URI를 필터링하도록 구현하였다
- /member/1, /member/2, /member/3, .....
@PostMapping("/login")
public String loginCheck(
@Validated @ModelAttribute(value = "loginDTO") LoginDTO loginDTO,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURI,
HttpServletRequest request
){
if(bindingResult.hasErrors()){
System.out.println(loginDTO);
return "login/loginForm";
}
Member loginMember = loginService.login(loginDTO.getLoginId(), loginDTO.getPassword());
log.info("login? = {}", loginMember);
if(loginMember == null){
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 올바르지 않습니다. 다시 확인해주세요");
return "login/loginForm";
}
HttpSession session = request.getSession();
session.setAttribute(SessionConstField.MY_SESSION_NAME, loginMember);
return "redirect:" + redirectURI;
}
@RequestParam(defaultValue = "/") String redirectURI
이전에 로그인필터에서 "sendRedirect("/login?redirectURI=" + requestURI)"을 통해서 /login 뒤에다가 쿼리 파라미터로 "이 사용자가 로그인 후에 redirect될 URI"를 붙여주었다
return "redirect:" + redirectURI
로그인에 성공했다면 해당 사용자는 이전에 Request했던 URI로 redirect된다
- 이전에 요청한것이 Home이였다면 그 역시 "defaultValue = "/"로 인해 return "redirect:/"로 구현이 되었다
(1) 로그인 하지 않은 상태에서 "/member/1"에 접근
"/member/1"은 로그인한 사용자만 접근할 수 있으므로 "로그인 필터"에서 미인증 사용자의 REQUEST를 잡아서 "/login"으로 redirect시켜준다
(2) "/login?=redirectURI=/member/1"에서 로그인 성공 후 "/member/1"로 redirect
로그인에 성공한 "admin"은 로그인 인증 필터를 통과해서 정상적으로 서블릿이 호출되고 컨트롤러가 호출됨에 따라 "/member/1"에 접근할 수 있다
초반에는 마이페이지 접근 URI가 "/members/1"이였지만 member 한명의 접근에 대한 URI이므로 "/member/1"로 변경