Filter
: Web Application에서 관리되는 영역으로써 Spring Boot Framework에서 Client 로 부터 오는 요청/응답에 대해서 최초/최종 단계의 위치에 존재하며, 이를 통해서 요청/응답의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인 할 수 있다.
Spring AOP의 단계부분에서의 데이터는 객체(엔티티)와 매핑이 되어있어 순수한 request/response값이 아니게 된다.
유일하게 ServletRequest, ServletResponse의 객체를 변환할 수 있다.
주로 Spring Framework에서는 request/response의 Logging 용도로 활용하거나 인증과 관련된 Logic들을 해당 Filter에서 처리
이를 선/후 처리함으로써 Service business logic과 분리시킴
필터 -> 디스패처 서블릿 -> 인터셉터 -> AOP 순의 라이플 사이클 확인
출처 : https://justforchangesake.wordpress.com/2014/05/07/spring-mvc-request-life-cycle/
Filter클래스에 @WebFilter(urlPatterns = “”) 로 해당 클래스에 지정
Controller 단에 @ServletComponentScan 선언 를 사용하여 별도의 필터(@WebFilter 스캔) 적용
FilterApplication.java
@SpringBootApplication
@ServletComponentScan
public class FilterApplication {
public static void main(String[] args) {
SpringApplication.run(FilterApplication.class, args);
}
}
GlobalFilter.java
/*
* javax.servlet.Filter
*/
@Slf4j
//@Component // 스프링에서 관리하기 위해 빈등록
@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;
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);
// 필터를 통해 스프링에 들어감
chain.doFilter(httpServletRequest,httpServletResponse);
// 요청 url
String url = httpServletRequest.getRequestURI();
// 요청 Body 내용
// BufferedReader br = httpServletRequest.getReader();
//
// br.lines().forEach(line->{
// log.info("url : {}, line : {}", url,line);
// });
/*
* java.lang.IllegalStateException: getReader() has already been called for this request
* filter 단에서 br.line or read를 통해서 요청Body 내용을 커서가 하나씩 읽어버리고 다 읽은 후 마지막 커서위치에서
* Controller의 JsonBody로 넘겼지만 이미 데이터를 읽은 후에 없는 내용을 전달하여 에러 발생
* HttpServlet을 캐싱이 되는 클래스로 생성(ContentCachingRequestWrapper,ContentCachingResponseWrapper), ByteArrayOutputStream 타입의 변수에 담아서 읽어드림
* 그럼에도 불구하고 에러 발생이 또 되는데 HttpMessageNotReadableException: Required request body is missing 으로 ContentCachingRequestWrapper 생성시 해당 정보의 길이만(내용x) 지정하고 있(클래스 생성자 참고)
* doFilter 이후 요청 정보를 조회
*/
// 후처리 정보
// request
String reqContent = new String(httpServletRequest.getContentAsByteArray());
log.info("request url : {}, requsetBody : {}", url, reqContent);
// response
String resContent = new String(httpServletResponse.getContentAsByteArray());
int httpStatusCode = httpServletResponse.getStatus();
/*
* getContentAsByteArray() 사용하여 response 정보를 ByteArray로 읽어버려서 커서가 끝까지 가버려서 빈 정보를 보냄
* copyBodyToResponse()를 사용하여 body 내용을 복사
*/
httpServletResponse.copyBodyToResponse();
log.info("response status : {}, responseBody : {}",httpStatusCode, resContent);
}
}
User.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private int age;
}
ApiController.java
/*
* @Slf4j
* lombok에서 제공하는 로거 log.info("a : {}",a);
*/
@Slf4j
@RestController
@RequestMapping("/api/user")
public class ApiController {
@PostMapping("")
public User user(@RequestBody User user){
log.info("User : {}",user.toString());
return user;
}
}
UserApiController.java
@Slf4j
@RestController
@RequestMapping("/api/temp")
public class UserApiController {
@PostMapping("")
public User user(@RequestBody User user){
log.info("User : {}",user.toString());
return user;
}
}
HTTP 요청
{
"name":"tom",
"age":10
}
HTTP 응답
{
"name":"tom",
"age":10
}
/api/user 결과
2022-11-14 16:43:27.660 INFO 38672 --- [nio-8080-exec-3] c.e.filter.interceptor.AuthInterceptor : request url : /api/user
2022-11-14 16:43:27.661 INFO 38672 --- [nio-8080-exec-3] c.e.filter.interceptor.AuthInterceptor : request qry : null
2022-11-14 16:43:27.661 INFO 38672 --- [nio-8080-exec-3] c.e.filter.interceptor.AuthInterceptor : has Annotation : false
2022-11-14 16:43:27.661 INFO 38672 --- [nio-8080-exec-3] c.e.filter.interceptor.AuthInterceptor : request uri : /api/user
2022-11-14 16:43:27.663 INFO 38672 --- [nio-8080-exec-3] c.e.filter.controller.ApiController : User : User(name=tom, age=10)
2022-11-14 16:43:27.663 INFO 38672 --- [nio-8080-exec-3] com.example.filter.filter.GlobalFilter : request url : /api/user, requsetBody : {
"name":"tom",
"age":10
}
2022-11-14 16:43:27.664 INFO 38672 --- [nio-8080-exec-3] com.example.filter.filter.GlobalFilter : response status : 200, responseBody : {"name":"tom","age":10}
/api/temp 결과 (WebFilter 적용)
2022-11-14 16:44:58.748 INFO 38672 --- [nio-8080-exec-4] c.e.filter.interceptor.AuthInterceptor : request url : /api/temp
2022-11-14 16:44:58.748 INFO 38672 --- [nio-8080-exec-4] c.e.filter.interceptor.AuthInterceptor : request qry : null
2022-11-14 16:44:58.748 INFO 38672 --- [nio-8080-exec-4] c.e.filter.interceptor.AuthInterceptor : has Annotation : false
2022-11-14 16:44:58.748 INFO 38672 --- [nio-8080-exec-4] c.e.filter.interceptor.AuthInterceptor : request uri : /api/temp
2022-11-14 16:44:58.750 INFO 38672 --- [nio-8080-exec-4] c.e.filter.controller.UserApiController : User : User(name=tom, age=10)
Interceptor
: Interceptor는 filter와 매우 유사한 형태로 존재하지만, 차이점은 Spring Context에 등록되어 Spring 기능이나 메소드 정보를 가지고 있다.
AOP와 유사한 기능을 제공 할 수 있으며, 주로 인증 단계를 처리하거나 Logging를 하는데 사용한다.
이를 선/후 처리함으로써, Service business logic과 분리
Auth.java
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}
AuthInterceptor.java
/*
* HandlerInterceptor
*/
@Component
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
/*
* Interceptor 권한 체크
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURI();
String qry = request.getQueryString();
log.info("request url : {} ",url);
log.info("request qry : {} ",qry);
// 권한 값 유무
boolean hasAnnotation = chechAnnotation(handler, Auth.class);
log.info("has Annotation : {} ",hasAnnotation);
URI uri = UriComponentsBuilder.fromUriString(url)
.query(qry)
.build()
.toUri();
log.info("request uri : {} ",uri);
// Auth 권한 체크
if(hasAnnotation){
// 예를 들어 파라미터로 확임함
String query = uri.getQuery();
if(query.equals("name=steve")){
log.info("Auth Check : true");
return true;
}
return false;
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
/*
* 어노테이션 유무 확인(핸들러, 어떤 어노테이션)
*/
public boolean chechAnnotation(Object handler, Class clazz){
// resource request 시 통과
if(handler instanceof ResourceHttpRequestHandler){
return true;
}
// annotation
HandlerMethod handlerMethod = (HandlerMethod) handler;
if(null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)){
// Method, Bean에 Auth Annotation이 있을 경우
return true;
}
// 조건에 맞지 않을 경우 false 하여 인터셉터 동작 불가
return false;
}
}
AppConfig.java
@Configuration
@RequiredArgsConstructor
public class AppConfig implements WebMvcConfigurer {
private final AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(authInterceptor);
// 특정 Path에서만 interceptor 동작하도록 설정
//registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*");
// 특정 Path에서 interceptor 동작제외
//registry.addInterceptor(authInterceptor).excludePathPatterns("/api/private/*");
}
}
PublicController.java
@RestController
@RequestMapping("/api/public")
@Slf4j
public class PublicController {
@GetMapping("/hello")
public String hello(){
log.info("public hello : ");
return "public hello";
}
}
PrivateController.java
@RestController
@RequestMapping("/api/private")
@Slf4j
@Auth // 어노테이션이 붙어있는 컨트롤러에서 세션 or 쿠키를 검사하여 컨트롤러로 통과시킬지 판단.
public class PrivateController {
@GetMapping("/hello")
public String hello(){
log.info("private hello : ");
return "private hello";
}
}
http://localhost:8080/api/public/hello?name=steve
2022-11-14 16:48:56.491 INFO 38672 --- [nio-8080-exec-6] c.e.filter.interceptor.AuthInterceptor : request url : /api/public/hello
2022-11-14 16:48:56.491 INFO 38672 --- [nio-8080-exec-6] c.e.filter.interceptor.AuthInterceptor : request qry : name=steve
2022-11-14 16:48:56.492 INFO 38672 --- [nio-8080-exec-6] c.e.filter.interceptor.AuthInterceptor : has Annotation : false
2022-11-14 16:48:56.493 INFO 38672 --- [nio-8080-exec-6] c.e.filter.interceptor.AuthInterceptor : request uri : /api/public/hello?name=steve
2022-11-14 16:48:56.494 INFO 38672 --- [nio-8080-exec-6] c.e.filter.controller.PublicController : public hello :
http://localhost:8080/api/private/hello?name=steve
2022-11-14 16:51:38.754 INFO 39426 --- [nio-8080-exec-2] c.e.filter.interceptor.AuthInterceptor : request url : /api/private/hello
2022-11-14 16:51:38.755 INFO 39426 --- [nio-8080-exec-2] c.e.filter.interceptor.AuthInterceptor : request qry : name=steve
2022-11-14 16:51:38.755 INFO 39426 --- [nio-8080-exec-2] c.e.filter.interceptor.AuthInterceptor : has Annotation : true
2022-11-14 16:51:38.755 INFO 39426 --- [nio-8080-exec-2] c.e.filter.interceptor.AuthInterceptor : request uri : /api/private/hello?name=steve
2022-11-14 16:51:38.755 INFO 39426 --- [nio-8080-exec-2] c.e.filter.interceptor.AuthInterceptor : Auth Check : true
2022-11-14 16:51:38.756 INFO 39426 --- [nio-8080-exec-2] c.e.filter.controller.PrivateController : private hello :
'Spring > 98. Infra' 카테고리의 다른 글
3. Spring Boot Validation과 Exception 예제 (0) | 2022.11.07 |
---|---|
2. Spring Boot Exception (0) | 2022.11.07 |
1. Spring Boot Validation (0) | 2022.11.07 |