Filter
- Web Application에서 관리되는 영역으로써 Spring Boot Framework에서 Client로 부터 오는 요청/응답에 대해서 최초/최종 단계의 위치에 존재하며 이를 통해 요청/응답의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인 할 수 있다.
- 유일하게 ServletRequest, ServletResponse의 객체를 변환 할 수 있다.
- 주로 Spring에서는 Requset/ Response의 Logging용도로 활용하거나, 인증과 관련된 Logic들을 해당 Filter에서 처리한다.
- 이를 선/후 처리함으로써 Service business logic과 분리 시킨다.
Filter 추가
- filter를 추기하기 위해서는 javax.servlet의 filter interface를 구현해야한다.
- filter interface에는 3개의 Mehtod가 있다.
- init 메소드
- 초기화 및 서비스에 추가히기위한 메소드이다.
- 웹 컨테이너가 1회 init 메소드를 호출하여 초기화하고 이후의 요청은 doFilter를 통해 전/후에 처리된다.
- doFilter 메소드
- urlPattern에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전/후에 실행된다.
- doFilter의 파라미터로인 FilterChain의 doFilter 통해 다음 대상으로 요청을 전달한다.
- destroy 메소드
- 자원 반환 및 서비스에서 제거하기 위한 메소드이다.
filter.GlobalFilter
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
@Slf4j
@Component
public class GlobalFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//전처리
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
String url = httpServletRequest.getRequestURI();
BufferedReader br = httpServletRequest.getReader();
br.lines().forEach(line -> {
log.info("url : {}, line : {}", url, line);
});
//BufferedReader를 forEach를 사용해 전부 확인하였기 때문에 seek가 가장 마지막에 가있다.
//전처리 후 Spring이 body를 가지고 User라는 객체에 Mapping을 시도하는데 이때 읽을 내용이 없기 때문에 Error가 발생한다.
chain.doFilter(request, response);
//후처리
}
}
- 위의 코드는 주석에 작성된 이유로 Error가 발생한다. 이러한 문제를 해결하기 위해 spring에서는 ContentCachingRequestWrapper, ContentCachingResponseWrapper를 지원한다.
1
2
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);
- ContentCaching…Wrapper는 처음 캐싱 처리가 될 때 Byte크기만 기록해 두고 내용은 기록되지 않는다. 내용은 해당 request, response가 inputstream으로 읽혀 졌을 때 채워지는데 request는 object mapping 이후에 수행된다. 따라서 이전에 전처리에 작성된 출력을 doFilter이후로 옮겨야 한다.
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
package com.example.filter.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
@Slf4j
@Component
public class GlobalFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//전처리
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);
chain.doFilter(httpServletRequest, httpServletResponse);
//후처리
String url = httpServletRequest.getRequestURI();
String reqContent = new String(httpServletRequest.getContentAsByteArray());
log.info("req url : {}, req body : {}", url, reqContent);
String resContent = new String(httpServletResponse.getContentAsByteArray());
int httpStatusCode = httpServletResponse.getStatus();
log.info("res status : {} , res body : {}", httpStatusCode, resContent);
}
}
- 원하는 Log들이 정상적으로 출력 되었지만 Client는 Body가 비어있는 Response 를 받게 된다. getContentAsByteArray로 response를 다 읽어버렸기 때문인데 copyBodyToResponse를 사용해 다시 body를 채워 넣는것으로 문제를 해결할 수 있다.
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
@Slf4j
@Component
public class GlobalFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//전처리
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);
chain.doFilter(httpServletRequest, httpServletResponse);
//후처리
String url = httpServletRequest.getRequestURI();
String reqContent = new String(httpServletRequest.getContentAsByteArray());
log.info("req url : {}, req body : {}", url, reqContent);
String resContent = new String(httpServletResponse.getContentAsByteArray());
httpServletResponse.copyBodyToResponse(); //이 부분 추가
int httpStatusCode = httpServletResponse.getStatus();
log.info("res status : {} , res body : {}", httpStatusCode, resContent);
}
}
원하는 곳에만 filter적용하기
- Application에 @ServletComponentScan 을 작성해준다.
- 기존 filter의 Component Annotation을 제거하고 @WebFilter(urlPatterns = “/api/user/*”) 처럼 url패턴으로 filter를 적용할 수 있다.
- WebFilter는 배열로 pattern을 넣어줄 수 있다.
Interceptor
- Interceptor란 filter와 매우 유사한 형태로 존재하지만 차이점은 Spring Context에 등록 된다는 점이다.
- filter와 다르게 spring이 제공하는 기술로 디스패처 서블릿이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조 및 가공할 수 있게해준다.
- AOP와 유사한 기능을 제공 할 수 있으며, 주로 인증 단계를 처리하거나 Logging을 하는 데에 사용한다.
- 이를 선/후 처리 함으로써, Service business logic과 분리 시킨다.
Interceptor 추가
- Interceptor를 추가하기 위해서는 org.springframework.web.servlet의 HandlerInterceptor interface를 구현해야한다. 이는 다음의 3가지 메소드를 가지고 있다.
- Interceptor interface에는 3개의 Method가 있다.
- preHandle 메소드
- Controller가 호출되기 전에 실행된다.
- true를 반환해야 다음 단계(다음 인터셉터 or 컨트롤러)로 진행이 된다.
- postHandle 메소드
- Controller 호출 후에 실행된다.
- afterCompletion 메소드
- 모든 작업이 완료된 후에 실행된다.
Interceptor 실습
- Interceptor 구현
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
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI();
URI uri = UriComponentsBuilder.fromUriString(request.getRequestURI()).query(request.getQueryString()).build().toUri();
log.info("request url : {}", url);
boolean hasAnnotation = checkAnnotation(handler, Auth.class);
log.info("has annotation : {}", hasAnnotation);
//서버는 모두 Public으로만 동작을 하고 Auth 권한을 가진 요청에 대해서는 세션, 쿠키 체크
if(hasAnnotation){
//권한 체크
String query = uri.getQuery();
log.info("query : {}", query);
if(query.equals("name=kms")){
return true;
}
throw new AuthException();
}
return true; // true가 되어야 interceptor를 넘어 내부 logic을 실행할 수 있다. False일 경우 return 된다.
}
private boolean checkAnnotation(Object handler, Class clazz){
// resource, javascript, html
if(handler instanceof ResourceHttpRequestHandler){
return false;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
if(handlerMethod.getMethodAnnotation(clazz) != null || handlerMethod.getBeanType().getAnnotation(clazz) != null){
return true;
}
return false;
}
}
- interceptor 추가
1 2 3 4 5 6 7 8 9 10 11 12
@Configuration @RequiredArgsConstructor //final로 생성된 객체를 생성자로 주입받을 수 있게 해준다. public class MvcConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor); // registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*"); -> 해당 path에서만 interceptor실행 } }
- test용 Controller추가
1 2 3 4 5 6 7 8 9 10
@RestController @RequestMapping("/api/private") @Auth public class PrivateController { @GetMapping("/hello") public String hello(){ return "private hello"; } }
1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api/public")
public class PublicController {
@GetMapping("/hello")
public String hello(){
return "public hello";
}
}
- Auth Annotation추가
1 2 3 4 5
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Auth { }
- Auth Exception
1
2
public class AuthException extends RuntimeException{
}
- Auth Exception handler
1
2
3
4
5
6
7
8
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AuthException.class)
public ResponseEntity authException(){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("");
}
}
- AuthException에 대해서 401 UNAUTHORIZED Response 리턴
- preHandle가 true를 리턴해야 내부 logic을 수행할 수 있다.
- preHandle의 Object handler는 ControllerMapping, Model, Data Binding, Type, Formatting 등 정보를 가지고 있다.
- checkAnnotation의 경우 handlerMethod를 사용해 Auth.class가 Annotation으로 들어있는지 확인해 True, False를 반환한다.
- 해당 메서드에서 True가 반환되면 Auth Annotation이 설정 되어 있는 것이고 이는 권한 체크가 필요함을 의미한다.
Filter와 Interceptor차이
관리되는 컨테이너
- Filter : Web Container
- Interceptor : Spring Container
용도
- Filter
- Spring과 무관하게 전역에서 공통으로 필요한 작업
- 데이터 압축 및 인코딩
- 보안관련 공통 작업(XXS 방)
- 모든 요청에 대한 로깅
- Interceptor
- 인증,인가 관련 공통작업
- Controller로 넘어가는 혹은 Controller에서 넘어오는 데이터의 가공
- API호출에 대한 로깅