minOS

로그인 처리2 - 서블릿 필터 소개 , 서블릿 필터 요청 로그 , 서블릿 필터 인증 체크 본문

TIL/김영한의 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술

로그인 처리2 - 서블릿 필터 소개 , 서블릿 필터 요청 로그 , 서블릿 필터 인증 체크

minOE 2024. 9. 27. 19:43
728x90

서블릿 필터 - 소개

필터는 서블릿이 지원하는 수문장이다.

필터의 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러

 

필터를 적용하면 필터가 호출 다음에 서블릿이 호출된다. 그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면

필터를 사용하면 된다. 참고로 필터는 특정 URL 패턴에 적용할 있다. `/*` 이라고 하면 모든 요청에 필터가 적용된다.


필터 제한

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():` 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.

 

 

서블릿 필터 -  요청 로그

필터가 정말 수문장 역할을 잘 하는지 확인하기 위해 가장 단순한 필터인, 모든 요청을 로그로 남기는 필터를 개발하고 적용해보자.


LogFilter - 로그 필터
package hello.login.web.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Slf4j
public class LogFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
        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() {
        Filter.super.destroy();
        log.info("log filter destroy");
    }
}​

 

- `public class LogFilter implements Filter {}`

     ㄴ 필터를 사용하려면 필터 인터페이스를 구현해야 한다.

-`doFilter(ServletRequest request, ServletResponse response, FilterChain chain)`

    ㄴ HTTP 요청이 오면 `doFilter` 호출된다.

    ㄴ `ServletRequest request` HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다. HTTP

          사용하면 `HttpServletRequest httpRequest = (HttpServletRequest) request;` 다운 캐스팅 하면 된다.

-`String uuid = UUID.randomUUID().toString();`

    ㄴ HTTP 요청을 구분하기 위해 요청당 임의의 `uuid` 생성해둔다.

-`log.info("REQUEST [{}][{}]", uuid, requestURI);`

     ㄴ`uuid` `requestURI` 출력한다.

- `chain.doFilter(request, response);`

     ㄴ 이 부분이 가장 중요하다. 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약

         로직을 호출하지 않으면 다음 단계로 진행되지 않는다.



WebConfig - 필터 설정
import hello.login.web.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean<Filter> logFilter(){
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");
        return filterFilterRegistrationBean;
    }
}​

로그 출력


필터를 등록할 때 `urlPattern` 을 `/*`로 등록했기 때문에 모든 요청에 해당 필터가 적용된다.


필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 `FilterRegistrationBean` 을 사용해서 등록하면 된다.
- `setFilter(new LogFilter())` : 등록할 필터를 지정한다.
- `setOrder(1)` : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
- `addUrlPatterns("/*")` : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.



참고
실무에서 HTTP 요청시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 logback mdc로 검색해보자

 

 

서블릿 필터 - 인증 체크

인증 체크 필터를 개발
로그인 되지 않은 사용자는 상품 관리 뿐만 아니라 미래에 개발될 페이지에도 접근하지 못하도록 하자




LoginCheckFilter - 인증 체크 필터
@Slf4j
public class LoginCheckFilter implements Filter {
    private static final String[] whitelist = {"/","members/add","/login","logout","/css/*"}; // 로그인 X 사용자 접근 가능
    @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);
                    //로그인으로 리다이렉트
                    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);`
    ㄴ
미인증 사용자는 로그인 화면으로 리다이렉트 한다. 그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾         아가야 하는 불편함이 있다. 예를 들어서 상품 관리 화면을 보려고 들어갔다가 로그인 화면으로 이동하면, 로그인 이후에 다시         상품 관리 화면으로 들어가는 것이 좋다. 이러한 기능을 위해 현재 요청한 경로인`requestURI` `/login` 쿼리 파라미           터로 함께 전달한다. 물론 `/login` 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능은 추가로 개발해야 한다.

- `return;`
  ㄴ여기가중요하다. 필터를 더는 진행하지 않는다. 이후 필터는 물론 서블릿, 컨트롤러가 더는호출되지않는다. 앞서 `redirect`        사용했기 때문에 `redirect` 응답으로 적용되고 요청이 끝난다.





WebConfig - loginCheckFilter() 추가
import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;

@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean<Filter> logFilter(){
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }
    @Bean
    public FilterRegistrationBean<Filter> loginCheckFilter(){
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
        filterFilterRegistrationBean.setOrder(2);
        filterFilterRegistrationBean.addUrlPatterns("/*"); //화이트 리스트 빼고 로그인 인증 다함
        return filterFilterRegistrationBean;
    }
}​



http://localhost:8080/items 로 접근하면 로그인 페이지로 리다이렉트 된다.


로그 출력

- `setFilter(new LoginCheckFilter())` : 로그인 필터를 등록한다.
- `setOrder(2)` : 순서를 2번으로 잡았다. 로그 필터 다음에 로그인 필터가 적용된다.
- `addUrlPatterns("/*")` : 모든 요청에 로그인 필터를 적용한다.



RedirectURL 처리
@PostMapping("/login")
    public String loginV4(@Validated @ModelAttribute LoginForm loginForm , BindingResult bindingResult,
                          @RequestParam(defaultValue = "/") String redirectURL,
                          HttpServletRequest request){
        if(bindingResult.hasErrors()){
            return "login/loginForm";
        }

        Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());

        if (loginMember == null){
            bindingResult.reject("loginFail","아이디 또는 비밀번호가 맞지 않습니다.");
            return "login/loginForm";
        }

        //로그인 성공 처리 TODO
        //세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
        HttpSession session = request.getSession();
        //세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER,loginMember);
        return "redirect:"+ redirectURL;

    }

로그인하면 바로 items 페이지로 이동


- 로그인 체크 필터에서, 미인증 사용자는 요청 경로를 포함해서 `/login` 에 `redirectURL` 요청 파라미터를 추가해서 요청했다.     이 값을 사용해서 로그인 성공시 해당 경로로 고객을 `redirect` 한다.




정리

서블릿 필터를 사용한 덕분에 로그인 하지 않은 사용자는 나머지 경로에 들어갈 없게 되었다. 공통 관심사를 서블

릿 필터를 사용해서 해결한 덕분에 향후 로그인 관련 정책이 변경되어도 부분만 변경하면 된다

728x90