Web Application에서 관리되는 영역으로서, Spring Boot Framework에서 Client로 부터 오는 요청/응답
에 대해서 최초/최종 단계의 위치에 존재
하며, 이를 통해 요청/응답의 정보
를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인 가능
-> 순수한 RequestBody와 클라이언트가 받을 ResponseBody를 확인할 수 있음
유일하게 ServletRequest
, ServletResponse
의 객체를 반환 할수 있음
주로 Request/Response의 Logging 용도
, 또는 인증과 관련된 Logic
들을 필터에서 처리
결과적으로 선/후 처리를 통해, Service Business Logic과 분리 시킴
Serlvet
: 클라이언트의 요청을 처리하고, 그 결과를 반환하는 Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술
user
package com.example.filter.dto;
import lombok.*;
// @Getter
// @Setter
@Data // getter,setter 뿐만 아니라 equals, hashCode,toString까지 오버라이딩 해줌
@NoArgsConstructor // 기본 생성자
@AllArgsConstructor // 전체 생성자 -> User(String,int)
public class User {
private String name;
private int age;
// 원래라면 여기다가 Get,Set 메서드에 toString 오버라이딩 하였음 -> 롬북 활용시 다름
// 실행될때는 필요X, 컴파일 시 자동으로 필요한 것들을 만들어 놓음
}
api controller
package com.example.filter.controller;
import com.example.filter.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j // 로그 남길때 System.out.println이 아닌 이 어노테이션 자주 사용
@RestController
@RequestMapping("/api/user")
public class ApiController {
@PostMapping("")
public User user(@RequestBody User user){
log.info("User: {}", user); // user.toString이 대입됨
return user;
}
}
Global Filter
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
// 필터를 특정 구역에만 적용할려면 1. Application에 @ServletComponentScan -> Filter엔 Component를
// WebFilter(urlPatterns = 적용 하고자 하는 디렉터리 범위) ex) WebFilter(urlPatterns="/api/user/*")
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);
// }); // br.read의 경우도 같은데 내용을 다 읽어서 커서가 라인에 젤 끝에 감
// 이를 해결하기 위해 ContentCachingRequestWrapper 사용
// 다 읽고나서 누군가 내용을 읽고자 하자면 미리 저장해놨던 내용 리턴
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);
// String url = httpServletRequest.getRequestURI();
// 내용을 담을 배열 크기만 잡히지 내용을 복사 해 놓지는 않아서 오류가 발생함!
// BufferedReader br = httpServletRequest.getReader();
//
// br.lines().forEach(line -> {
// log.info("url : {}, line : {}",url , line);
// });
// chain.doFilter 위가 전처리 구간
chain.doFilter(httpServletRequest, httpServletResponse);
// chain.doFilter 밑이 후처리 구간(내부 스프링 안으로 들어감)
String url = httpServletRequest.getRequestURI();
String reqContent = new String(httpServletRequest.getContentAsByteArray());
log.info("request url: {}, request body : {}", url, reqContent);
String resContent = new String(httpServletResponse.getContentAsByteArray()); // get에서 body내용을 다 빼버림
int httpStatus = httpServletResponse.getStatus(); // 이까지만 하면 내용을 다 읽고 커서가 끝으로 간 상태 -> 복사해주자
httpServletResponse.copyBodyToResponse(); // 이 부분 을 해야 클라이언트가 올바르게 응답 받음
log.info("response status: {} responseBody : {}", httpStatus, resContent);
}
}
Filter와 유사한 형태로 존재하지만, Spring Context
에 등록되는것이 차이점
AOP와 유사한 기능을 제공할 수 있으며, 주로 인증단계
를 처리하거나 + Logging을 할때 사용
이를 선/후 처리 함으로서 Service Business Logic과 분리 시킨다
Auth
package com.example.intercepter.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}
MVC config
package com.example.intercepter.config;
import com.example.intercepter.intercepter.AuthIntercepter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor // MvcConfig(AuthIntercepter) -> 순환 참조 막기위해
public class MvcConfig implements WebMvcConfigurer {
private final AuthIntercepter authIntercepter;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authIntercepter).addPathPatterns("/api/private/*");
// 여기다 다른 add 를 추가하면 위에꺼 동작 후 동작
}
}
private Controller
package com.example.intercepter.controller;
import com.example.intercepter.annotation.Auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// 인증된 사용자만 사용할 수 있도록
@RestController
@RequestMapping("/api/private")
@Auth
@Slf4j
public class privateController {
@GetMapping("/hello")
public String hello(){
log.info("private hello controller");
return "private hello";
}
}
public Controller
package com.example.intercepter.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// 모두가 들어오게끔 하는 컨트롤러
@RestController
@RequestMapping("/api")
public class publicController {
@GetMapping("/hello")
public String hello(){
return " public hello:";
}
}
Auth Exception
package com.example.intercepter.exception;
public class AuthException extends RuntimeException{
}
global Exception handler
package com.example.intercepter.handler;
import com.example.intercepter.exception.AuthException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AuthException.class)
public ResponseEntity authException(){
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
Auth Intercepter
package com.example.intercepter.intercepter;
import com.example.intercepter.annotation.Auth;
import com.example.intercepter.exception.AuthException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URI;
@Slf4j
@Component
// 인터셉터는 등록 안해주면 동작을 안함 -> MVC config
public class AuthIntercepter implements HandlerInterceptor {
// filter는 핸들러 없음
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 여기서도 request와 reponse를 읽으면 내용이 손실 됨
// 따라서 filter단에서 ContentCachingRequestWrapper 나 response를 만들어 doFilter에 넣어주게 되면
// 이 인터셉터에서 형변환이 가능
//ex) (ContentCachingRequestWrapper) request;
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);
// 처음엔 privateController 는 true, public은 false;
// return false; // Controller까지 도달 못함
// 나의 서버는 모두 퍼블릭으로 동작을 하는데
// 단 Auth 권한을 가진 요청에 대해서는 세션,쿠키,etc... 등의 정책
if(hasAnnotation){
// 권한 체크
String query = uri.getQuery();
log.info("query : {}", query);
if(query.equals("name=steve")){
return true;
}
throw new AuthException();
}
return true;
}
private boolean checkAnnotation(Object handler, Class clazz){
// resource : javascript or html
if(handler instanceof ResourceHttpRequestHandler){
return true;
}
// 어노테이션 달렸는지 체크
HandlerMethod handlerMethod = (HandlerMethod)handler;
if(null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)){
// Auth Annotation이 있을 때는 true
return true;
}
return false;
}
}