spring

로그인 - 필터, 인터셉터

키위먹고싶다 2022. 2. 5. 21:17

상품 등록, 수정, 삭제, 조회등에 여러 로직에서 인증에 대한 공통 관심을 가지고 있는데

이러한 웹과 관련된 공통 관심사는 서블릿 필터, 스프링 인터셉터를 사용하는 것이 좋다. 공통 관심사를 처리하기 위해 헤더나 url정보가 필요한데 서블릿 필터나 스프링 인터셉터는 HttpServletRequest를 제공한다. 

 

필터

 

 http요청 -> was -> 필터 -> 서블릿 -> 컨트롤러 

필터에 로그인한 여부를 체크하는 로직을 넣으면 로그인 한 사용자를 체크 할 수 있다.

 

package hello.itemservice.web.filter;

import hello.itemservice.web.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@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 (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("인증 체크 필터 종료 {} ", request);
        }
    }

    /**
     * 화이트 리스트의 경우 인증 체크X
     */
    private boolean isLoginCheckPath(String requestURI){
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
    }

}

 

 

Filter인터페이스를 구현한다. 

whitelist의 경로를 제외한 나머지 요청이(등록, 수정 등) 오면 세션을 확인하고 세션이 없으면 로그인 페이지로 이동한다.

 

package hello.itemservice;

import hello.itemservice.web.filter.LogFilter;
import hello.itemservice.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 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을 사용한다. 

setOrder는 필터가 적용되는 순서를 결정하는데 필터에는 필터체인이라는 기능이 있어서 만약 필터가 여러개일 경우

 

http요청 -> was -> 필터1 -> 필터2 -> 서블릿 -> 컨트롤러 순으로 이동함.

 

@PostMapping("/login")
    public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, 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();
        //세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

        return "redirect:/";
    }

 

그런데 만약 로그인하지 않은 사용자가 상품조회를 하기 위해 상품조회 페이지를 요청하면 로그인화면으로 이동하고 로그인을 성공하면 홈화면으로 이동해버린다. 그럼 사용자가 원래 요청했던 상품조회페이지를 다시 요청해야 하는 번거로움이 생긴다. 

 

@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();
    //세션에 로그인 회원 정보 보관
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

    return "redirect:" + redirectURL;
}

 

그래서 필터인증을 거치고 리다이렉트의 파라미터에 원래 요청했던 uri정보를 쿼리 파라미터로 보내서 컨트롤러의 과정을 거치고 다시 그 요청페이지로 이동하도록 리다이렉트 한다. 

 

만약 필터가 없다면 컨트롤러에 일일이 로그인을 검증하는 로직을 넣어야 하는데 만약 로직이 변경될 경우 매우 번거롭다. 그러나 필터가 있으면 필터하나를 변경해서 모든 요청에 적용할 수 있고 또 필터의 패턴을 적용해서 url경로의 범위도 지정할 수 있어 편리하다. 

 

스프링 인터셉터