[Spring] Filter & interceptor 활용

WOOK JONG KIM·2022년 10월 24일
0

패캠_java&Spring

목록 보기
46/103
post-thumbnail

Filter

Web Application에서 관리되는 영역으로서, Spring Boot Framework에서 Client로 부터 오는 요청/응답에 대해서 최초/최종 단계의 위치에 존재하며, 이를 통해 요청/응답의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인 가능

-> 순수한 RequestBody와 클라이언트가 받을 ResponseBody를 확인할 수 있음

유일하게 ServletRequest, ServletResponse의 객체를 반환 할수 있음

주로 Request/Response의 Logging 용도, 또는 인증과 관련된 Logic들을 필터에서 처리

결과적으로 선/후 처리를 통해, Service Business Logic과 분리 시킴

Serlvet :  클라이언트의 요청을 처리하고, 그 결과를 반환하는  Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술

Filter 활용 예시

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);

    }
}

Intercepter

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;
    }
}
profile
Journey for Backend Developer

0개의 댓글