SpringBoot(6) Filter와 Interceptor, 전처리와 후처리, 권한 부여

Yeppi's 개발 일기·2022년 7월 4일
0

Spring&SpringBoot

목록 보기
14/16
post-custom-banner

1. Filter

1) Filter 란?

Servlet&JSP 시리즈의 9.필터(Filter) 와 리스너(Listener) 개념 및 실습을 참고하자

  • Filter Web Application 에서 관리되는 영역
  • Spring Boot Framework 에서 Client로 부터 오는 요청/응답에 대해서
    최초/최종 단계의 위치에 존재
  • 요청/응답의 정보를 변경
    or
    Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인
  • 유일하게 ServletRequest, ServletResponse 의 객체 변환 가능
  • 주로 Spring Framework 에서는
    request / responseLogging 용도로 활용하거나,
    인증과 관련된 Logic 들을 해당 Filter에서 처리 한다.(보안처리)
    👉 이를 선/후 처리 함으로써, Service business logic 과 분리 시킨다.




2) 실습

인텔리제이를 사용한다

📌실습 전 세팅📌

  • 이번 실습에서는 Lombok 사용

  • build.gradle 파일에 가보면 잘 추가되어있음

    • 컴파일할때 작동

      dependencies {
          implementation 'org.springframework.boot:spring-boot-starter-web'
          compileOnly 'org.projectlombok:lombok'
          annotationProcessor 'org.projectlombok:lombok'
          testImplementation 'org.springframework.boot:spring-boot-starter-test'
      }
  • lombok 사용한 User.java

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private String name;
        private int age;
    }
  • ApiController.java
    • lombok을 이용한 객체 사용시 @Slf4j 를 사용

    • 시스템 아웃말고 log 사용가능

      @Slf4j
      @RestController
      @RequestMapping("/api/user")
      public class ApiController {
      
      		@PostMapping("")
          public User user(@RequestBody User user) {
              log.info("User : {}", user);
              return user;
          }
      }



📌필터 사용하기📌

  • GlobalFilter.java
    버퍼로 데이터 값 읽어오기
    @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);
            });
    
            chain.doFilter(request, response);
            // 후처리
            // 다음에
    
        }
    }

  • POST 방식으로 브라우저에 요청하면? 에러 발생
    {
      "name" : "yeppi",
      "age" : 11
    }

  • 파란 부분처럼 값을 라인마다 읽어오긴 했지만, 예외가 발생함
  • 왜일까?
    자바는 커서 방식으로 데이터를 차례대로 읽어들이는데,
    BufferedReader 에서 이미 데이터를 끝까지 읽어버린 상태이다.
    👉 따라서 그 뒤로 데이터를 뽑아내려고하면, 커서가 가장 밑에 있기 때문에 더 이상 읽어들일 데이터가 없다

  • 위 문제 해결하려면?
    Filter 파일에 전처리를 캐싱 객체로 처리 해주면 됨
    캐시로 한 번 읽어들인 데이터를 기억시키도록 하자
    // 전처리
    ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
    ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);



📌전처리와 후처리📌

  • 후처리는 꼭 doFilter()실행된 후에 작성해야 함(위 예제 참고)
    • 전처리에 모든 것을 처리한다고해서 실질적으로 캐싱 객체에 다 담기는 것이 아니기 때문
    • 따라서 ❗ 후처리에서 모든 정보를 기록하면 된다 ❗
  • GlobalFilter.java
    @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);
    
            String url = httpServletRequest.getRequestURI();
    
            chain.doFilter(httpServletRequest, httpServletResponse);
    
            // 후처리
            String reqContent = new String(httpServletRequest.getContentAsByteArray());
            log.info("response url : {}, responseBody : {}", url, reqContent);
    
            String resContent = new String(httpServletResponse.getContentAsByteArray());
            int httpStatus = httpServletResponse.getStatus();
            log.info("response status : {}, responseBody : {}", httpStatus, resContent);
        }
    }
  • POST 방식으로 브라우저에 요청하면? 잘 동작
    {
      "name" : "yeppi",
      "age" : 11
    }
  • 콘솔창에도 잘 출력

  • 이때, 위 Response body(error 200 녹색 사진 참고)가 비어있다.
    • 앞서 설명했던 것처럼 데이터를 이미 다 읽어버렸기 때문에 발생한 문제와 동일한 문제이다
    • 다시 데이터를 복사해서 저장해주자 httpServletResponse.copyBodyToResponse();
  • GlobalFilter.java
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            // 전처리
    				. . .
    
            // 후처리
            . . .
            String resContent = new String(httpServletResponse.getContentAsByteArray());
            int httpStatus = httpServletResponse.getStatus();
    
            httpServletResponse.copyBodyToResponse(); // 복사
    				
    				. . .
    
        }
    
  • 같은 방식으로 브라우저에 출력하면 클라이언트는 성공적으로 데이터를 받게 되는 것을 확인!



📌특정 컨트롤러에만 필터 적용📌

  • ApiUserController.java
    • ApiController.java 를 복사해서 temp 로 변경

      @Slf4j
      @RestController
      @RequestMapping("/api/temp")
      public class ApiUserController {
      
          @PostMapping("")
          public User user(@RequestBody User user) {
              log.info("temp : {}", user);
              return user;
          }
      }

  • FilterApplication.java
    • @ServletComponentScan 을 추가

  • GlobalFilter.java
    • @Component 대신 @WebFilter 사용

    • user 하위의 모든 주소 매칭 ⇒ temp 는 매칭되지 않음

      @Slf4j
      @WebFilter(urlPatterns = "/api/user/**")
      public class GlobalFilter implements Filter { . . . }



2. Interceptor

1) Interceptor 란?

  • Filter와 매우 유사한 형태로 존재

  • Filter와 차이점은 Spring Context에 등록되다는 것

  • AOP와 유사한 기능 제공

  • 인증 단계를 처리 or Logging 처리

👉 이를 선/후 처리 함으로써, Service business logic과 분리 시킨다





2) 실습

인텔리제이를 사용한다

📌@Annotation 사용📌

  • 어노테이션을 직접 만들어서 private 클래스로 사용해보자
  • 브라우저에서 요청 시, url 데이터 출력
  • @RequiredArgsConstructor 생성자에서 동작
  • AuthInterceptor
    @Slf4j
    @Component
    public class AuthInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            String url = request.getRequestURI();
            log.info("request url : {}", url);
    
            return true;
            // return false; // 동작 안함
        }
    
        private boolean checkAnnotation(Object handler, Class clazz) {
            // resource javascript, html 등
            if(handler instanceof ResourceHttpRequestHandler) {
                return true;
            }
    
            // annotation check
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            if(null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)) {
                // Auth annoation 이 있을 때
                return true;
            }
    
            return false;
    
        }
    }

  • 위 어노테이션 사용하는 private 클래스
    • @Auth 는 특정 클래스나 특정 메서드를 검사하고 싶을 때 사용

    • hello()에 사용할 수 있음

      @RestController
      @RequestMapping("/api/private")
      @Auth
      @Slf4j
      public class PrivateController {
      
          @GetMapping("/hello")
          public String hello() {
              log.info("private hello controller");
              return "private hello";
          }
      }
  • MvcConfig
    @Configuration
    @RequiredArgsConstructor
    public class MvcConfig implements WebMvcConfigurer {
    
        private final AuthInterceptor authInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(authInterceptor);
        }
    }
  • GET 방식으로 브라우저에 url 입력시



📌권한 지정📌

  • 모든 url 을 대상으로
  • 어노테이션 이용하여 권한 지정
  • AuthInterceptor
    @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); // private true, public false
    
            // 나의 서버는 모두 public으로 동작을 하는데
            // 단, Auth 권한을 가진 요청에 대해서는 세션, 쿠키 등을 검사
            if(hasAnnotation) {
                // 권한 체크
                String query = uri.getQuery();
                log.info("query = {}", query);
                if(query.equals("name=yeppi")) { // 이름이 yeppi 일때만 통과
                    return true;
                }
    
                return false;
            }
    
           return true;
            // return false; // 동작 안함
        }
    
        private boolean checkAnnotation(Object handler, Class clazz) {
            . . .
        }
    }

  • 브라우저 및 콘솔창 결과

  • 특정 url 대상으로만 권한 설정을 하고 싶다면?
  • 바로 위 실습의 MvcConfig,java 에 .addPathPatterns 추가
    registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*");
  • 위에서 실습했던 것처럼
    어노테이션을 사용한 권한체크를 하지 않고도 권한을 체크할 수 있다.



📌권한없을 때 예외처리📌

  • AuthInterceptor
    @Slf4j
    @Component
    public class AuthInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String url = request.getRequestURI();
    
    				. . .
    
            if(hasAnnotation) {
                // 권한 체크
                . . .
    
                // 권한 없다면
                throw new AuthException();
            }
    
           return true;
            // return false; // 동작 안함
        }
    
        private boolean checkAnnotation(Object handler, Class clazz) {
            . . .
        }
    }

  • GlobalExceptionHandler
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(AuthException.class)
        public ResponseEntity authException() {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); // UNAUTHORIZED 401 error
        }
    }

  • 브라우저에 이름이 yeppi 가 아닌 데이터를 쏘면?


🍑 정리 🍑

특정 권한은? 인터셉터에서 검사

  • 필터는 불가능
  • 스프링Context 에서 관리가 되고 있음으로
    어떤 클래스/컨트롤러의 데이터 흐름을 어노테이션으로 관리할 수 있음

필터

  • 웹 애플리케이션에서 관리
  • handler 오브젝트는 없음

VS

인터셉터

  • 스프링Context 에서 관리
  • 어노테이션, 클래스 사용 가능
  • handler 오브젝트 사용 가능
profile
imaginative and free developer. 백엔드 / UX / DATA / 기획에 관심있지만 고양이는 없는 예비 개발자👋
post-custom-banner

0개의 댓글