Updated:

1. 개요

웹과 관련된 공통 관심사는 HttpServletRequest를 제공하는 서블릿 필터나 스프링 인터셉터를 이용하여 처리할 수 있다. 이번에는 필터와 인터셉터를 사용하는 방법에 대해 알아보도록 하자.

2. 개발 환경

  • Java 11

  • Spring Boot 2.7.5

3. 필터

필터를 통해 서블릿으로 들어오기 전의 공통 관심사를 처리할 수 있다. 필터는 체인으로 구성되며, 중간에 여러 필터를 자유롭게 추가할 수 있다. 필터 흐름은 아래와 같다.

1) 정상 처리

HTTP요청 → WAS → 필터1 → 필터2 → 필터3 → 서블릿 → 컨트롤러

2) 비정상 처리

HTTP요청 → WAS → 필터1 → 필터2 → 필터3

3-1. 필터 인터페이스

1
2
3
4
5
6
7
8
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() : 필터 종료 메서드로, 서블릿 컨테이너 종료 시 호출

3-2. 예제코드

[LoginCheckFilter.java]

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
32
33
34
35
36
37
38
39
@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);
                    httpResponse.sendRedirect("/login?redirectURL=" + requestURI); // 로그인으로 redirect
                    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);
    }
}

Line 25 : 다음 필터가 있으면 필터를 호출하고, 없으면 서블릿 호출

[WebConfig.java]

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebConfig implements WebMvcConfigurer {

    public FilterRegistrationBean loginCheckFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new LoginCheckFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}

Line 6 : 등록할 필터 설정

Line 7 : 필터의 순서 설정 (숫자가 작을수록 먼저 동작)

Line 8 : 필터를 적용할 URL 패턴 지정

[LoginController.java]

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
32
33
34
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;

    @GetMapping("/login")
    public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
        return "login/loginForm";
    }

    @PostMapping("/login")
    public String loginV4(@Valid @ModelAttribute("loginForm") 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;
    }

[LoginService.java]

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@RequiredArgsConstructor
public class LoginService {

    private final MemberRepository memberRepository;

    public Member login(String loginId, String password) {
        return memberRepository.findByLoginId(loginId)
                .filter(m -> m.getPassword().equals(password))
                .orElse(null);
    }
}

[SessionConst.java]

1
2
3
4
public class SessionConst {

    public static final String LOGIN_MEMBER = "loginMember";
}

[login/loginForm.html]

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
        .container {
            max-width: 560px;
        }
        .field-error {
            border-color: #dc3545;
            color: #dc3545;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>로그인</h2>
    </div>
    <form action="item.html" th:action th:object="${loginForm}" method="post">
        <div th:if="${#fields.hasGlobalErrors()}">
            <p class="field-error" th:each="err : ${#fields.globalErrors()}"
               th:text="${err}">전체 오류 메시지</p>
        </div>
        <div>
            <label for="loginId">로그인 ID</label>
            <input type="text" id="loginId" th:field="*{loginId}" class="form-control" th:errorclass="field-error">
            <div class="field-error" th:errors="*{loginId}" />
        </div>
        <div>
            <label for="password">비밀번호</label>
            <input type="password" id="password" th:field="*{password}" class="form-control" th:errorclass="field-error">
            <div class="field-error" th:errors="*{password}" />
        </div>
        <hr class="my-4">
        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">로그인</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/}'|" type="button">취소</button>
            </div>
        </div>
    </form>
</div> <!-- /container -->
</body>
</html>

4. 인터셉터

인터셉터를 통해 필터와 마찬가지로 공통 관심사를 처리할 수 있다. 필터는 서블릿이 제공하는 기술인 반면에, 인터셉터는 스프링 MVC가 제공하는 기술이라는 점이 다르다. 인터셉터 호출은 아래와 같다.

1) 정상 처리

HTTP요청 → WAS → 필터 → 서블릿 → 인터셉터1 → 인터셉터2 → 컨트롤러

2) 비정상 처리

HTTP요청 → WAS → 필터 → 서블릿 → 인터셉터1 → 인터셉터2

4-1. 인터셉터 인터페이스

1
2
3
4
5
6
7
8
9
10
public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		return true;
	}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {}

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}
}
  • preHandle() : 컨트롤러 호출 전 호출되는 메서드

  • postHandle() : 컨트롤러 호출 후 호출되는 메서드로, 컨트롤러에서 예외 발생 시 호출되지 않음

  • afterCompletion() : 뷰 렌더링 이후 호출되는 메서드로, 예외와 상관없이 항상 호출 (예외 발생 시 예외 정보를 포함해서 호출)

4-2. 예제 코드

[LoginCheckInterceptor.java]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        log.info("인증 체크 인터셉터 실행 {}", requestURI);

        HttpSession session = request.getSession();
        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info("미인증 사용자 요청");
            // 로그인으로 redirect
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }

        return true;
    }
}

[WebConfig.java]

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/add", "/login", "/logout", "/css/**", "/*.ico", "/error");
    }
}

Line 6 : 등록할 인터셉터 설정

Line 7 : 인터셉터의 순서 설정 (숫자가 작을수록 먼저 동작)

Line 8 : 인터셉터를 적용할 URL 패턴 지정

Line 9 : 인터셉터를 적용하지 않을 URL 패턴 지정

[SessionConst.java]

1
2
3
4
public class SessionConst {

    public static final String LOGIN_MEMBER = "loginMember";
}

Updated:

Leave a comment