Filter란?
Web Application에서 관리되는 영역(Handler object같은 것이 없음)으로써 Spring Boot Framework에서 Client로부터 오는 request/response에 대해서 최초/최종 단계의 위치에 존재하며, 이를 통해서 request/response의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인할 수 있다.
@Slf4j // logging을 위한 annotation
@RestController
@RequestMapping("/api/filter")
public class FilterApiController {
@PostMapping("/post")
public User user(@RequestBody User user){
log.info("User : {}",user); // {}안에 객체의 toString()이 매칭이됨
return user;
}
}
//@Component // spring에 등록하는 방식, 모든 controller에 동작하게됨
@Slf4j
@WebFilter(urlPatterns = "/api/filter/post/*") // 특정 class에만 filter를 적용시키고 싶을 때, urls : 여러개를 설정할 수 도 있음
public class GlobalFilter implements Filter { // javax.servlet.* 상속
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 전처리
// ContentCaching : 내용을 미리 담고 커서가 이동하면서 읽음 -> 실질적인 데이터의 커서가 이동x
// 하지만 맨처음에는 길이기반으로 초기화되고 ContentCachingRequestWrapper 내부에서 read,write하는 부분은 따로 있음음
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);
// doFilter를 통해 내부로 들어가 메서드가 실행되고 그 결과 content를 읽을 수 있음
// 따라서 doFilter 이후에 log를 찍어봐야함!!!!
chain.doFilter(httpServletRequest, httpServletResponse);
String url = httpServletRequest.getRequestURI();
// 후처리
// 위에서 읽은 내용을 logging 할 수 있음
String reqContent = new String(httpServletRequest.getContentAsByteArray());
log.info("request : {}, requestBody : {}",url, reqContent);
String resContent = new String(httpServletResponse.getContentAsByteArray());
int httpStatus = httpServletResponse.getStatus();
// getContentAsByteArray()로 다 읽어버려서 body의 커서가 끝까지 내려감...
// 따라서 읽은만큼 복사해줘야 client가 제대로된 response를 받을 수 있음
httpServletResponse.copyBodyToResponse();
log.info("response status : {}, responseBody : {}",httpStatus, resContent);
}
}
Interceptor란?
Filter와 매우 유사한 형태로 존재하지만, 차이점은 Spring Context에 등록되기 때문에 annotation, class들을 활용할 수 있다.
AOP와 유사한 기능을 제공할 수 있으며, 주로 인증 단계를 처리하거나, Logging을 하는데에 사용한다.
이를 선/후 처리함으로써, Service business logic과 분리시킨다.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target( {ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}
@RestController
@RequestMapping("/api/private")
@Auth
@Slf4j
public class PrivateController { // 내부 사용자 혹은 세션이 인증된 사용자만 사용
// @Auth 특정 메서드에 걸어줄 수 도 있지만 이런 방식은 유지보수가 어렵기 때문에
// controller에 걸어주거나 특정 url에 매칭시켜주는것이 좋음
@GetMapping("/hello")
public String hello(){
log.info("private hello controller");
return "private hello";
}
}
vs
@RestController
@RequestMapping("/api/public")
public class PublicController { // Open API, 아무나 사용가능
@GetMapping("/hello")
public String hello(){
return "public hello";
}
}
@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 원래대로라면 filter에서와 했던 것과 마찬가지로 ContentCachingRequestWrapper로 형변환하고
// doFilter메서드를 실행해야함(request의 커서가 다 이동되서 읽을 내용이 없어지는 이슈를 해결하기 위함)
// filter에서 전처리 -> interceptor에서 값을 받아서 형변환해야함
// 그렇지 않으면 HttpServletRequest를 interceptor에서 형변환해줄 수 없음
// 하지만 이 코드에서는 일단은 filter의 전처리 없이 그냥 진행
String url = request.getRequestURI();
URI uri = UriComponentsBuilder.fromUriString(request.getRequestURI())
.query(request.getQueryString())
.build().toUri(); // uri 에서 query 파싱
log.info("request : {}", url);
boolean hasAnnotation = checkAnnotation(handler, Auth.class);
log.info("has annotation : {}", hasAnnotation);
// 나의 서버는 모두 public으로 동작을 하는데
// 단! Auth 권한을 가진 요청에 대해서는 세션, 쿠키등을 검사함
if(hasAnnotation){ // 우리가 원하는 annotation으로 처리하는 방식
// 권한체크
String query = uri.getQuery(); // 쿼리가 같으면 통과? 보통은 쿠키나 세션을 검사함
log.info("query : {}", query);
if(query.equals("name=steve")){
return true;
}
throw new AuthException(); // 권한이 없음을 예외처리
// return false;
}
return true; // true가 되어야 interceptor를 통과할 수 있음음
}
private boolean checkAnnotation(Object handler, Class clazz){
// resource(js, html)은 무조건 통과
if(handler instanceof ResourceHttpRequestHandler){
return true;
}
// annotation check
HandlerMethod handlerMethod = (HandlerMethod) handler;
// annotation이 달려있는지 check
if(null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)){
// Auth annotation이 있을 때는 true
return true;
}
return false;
}
}
@Configuration
@RequiredArgsConstructor // final로 선언된 객체들을 생성자로 주입받을 수 있도록 함
public class MvcConfig implements WebMvcConfigurer {
// @Autowired 로도 받을 수 있지만 순환 참조가 일어날 수 도 있음
private final AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns으로 특정 url에 대해서만 AuthInterceptor를 동작하게 할 수 있음, 필요한 주소들을 여러개 추가할 수도 있음
// registry.addInterceptor(authInterceptor).excludePathPatterns("/api/private/*"); 다음과 같이 제외할 수도 있음
// 여러가지 interceptor가 동작할 수 있는데 그것은 코딩된 순서대로 동작함 -> 인증과정 depth 구현가능!
registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*");
}
}
출처 : 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online.