[Spring] 필터, 인터셉터
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";
}
Leave a comment