본문 바로가기

개인적으로 공부한 것을 정리해 놓은 블로그입니다 틀린 것이 있으면 댓글 부탁 드립니다!


SPRING BOOT

Spring boot 공부 6 - interceptor

반응형

interceptor

 

interceptor는 Filter 와 매우 유사하나 차이점은 ,Web Application context 에서 돌아가는 filter와는 다르게 

 

Spring Context에 등록 되고 그 안에서 동작하기 때문에 Spring Context에 관한 내용을 사용할 수 있다 .

 

 

필터와 마찬가지로 선/후 처리가 가능하기 때문에 , 인증 단계를 처리하거나 , Logging을 

 

Service business logic으로 부터 분리 시킬 수 있다 .

 

 

Interceptor 구현  

 

 

먼저 인터셉터를 탈 PrivateController와  모두에게 공개되는 PublicController를  만들었다.

 

 

PrivateController

 

//인증된 사용자만 접근할 수 있는 컨트롤러
@RestController
@RequestMapping("/api/private")
@Auth
public class PrivateController {

    @GetMapping("/hello")
    public String hello(){
        return  "public hello";
    }

}

 

PublicController

 

//누구나 사용할 수 있는 openAPI 컨트롤러
@RestController
@RequestMapping("/api/public")
public class PublicController {

    @GetMapping("/hello")
    public String hello(){
        return "public hello";
    }
}

 

PrivateController에 붙어있는 @Auth(직접 정의한 annotation)를 통해 해당 어노테이션이 있는 컨트롤러의 요청경로로 들어오면 인증 과정을 거쳐야한다. 간단하게 그냥 String 값만 비교해보겠다 .

 

AuthInterceptor

 

package com.example.hello.interceptorTest.interceptor;

import com.example.hello.interceptorTest.annotation.Auth;
import com.example.hello.interceptorTest.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.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URI;

@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //필터 -> dispatcherServlet - > Interceptor 순으로 내려오기때문에
        //필터에서 ContentCachingRequestWrapper 타입으로 변환했다면
        //인터셉터에서 requset 를 ContentCachingRequestWrapper로 형변환하여 받을 수 있다 .

        //uri를 인터셉터에서 가져왔다.
        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("hasAnnotation:{}",hasAnnotation);

        //서버가 모두 public으로 동작하지만
        //auth 권한을 가진 요청에 대해서는 세션, 쿠키 등을 확인하겠다

        if(hasAnnotation){
            //권한체크
            String query = uri.getQuery();
            log.info("query:{}",query);
            if(query.equals("name=ugo")){
                return true;
            }
            throw new AuthException("ugo가 아님");
        }


        //인터셉에서 중요한 것은 handler다
        //handler는 데이터 바인딩 , validation , 컨트롤러 핸들러 맵핑 등 정보가 드렁있다 .

        return true;
    }

    //Class 클래스는 자바어플리케이션에서 돌아가는 클래스, 인터페이스 ,어노테이션에 대한 정보를 가지고 있다.
    private boolean checkAnnotation(Object handler, Class clazz){

        //요청하는 것이 resource javascript ,html 일 경우 무조건 통과
        if(handler instanceof ResourceHttpRequestHandler){
            return true;
        }

        //annotation check
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        String name = method.getName();
        log.info("name:{}", name);
        //Auth1 annotation 을 가지고 있는지 없는지 확인
        if(null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)){
            return true;
        }

        return false;
    }
}

 

 

인터셉터의 코드를 위에서부터 살펴보자 인터셉터는 HandlerInterceptor를 구현한다. 

 

인터셉터는 @Component 등록을 해줘야 bean으로 관리된다. 

 

preHandler 메서드는 컨트롤러 단으로 요청이 넘거가기 전에 잡아낸다 . 

 

위에 적혀 있듯이  Filter 가 존재한다면 Filter를 거쳐서 Interceptor로 넘어오기 떄문에  filter의 chain.doFilter(request ,response)안에 request 와 response가 preHandler의 request , response가 되는 것이다 .

 

넘어온 요청에서 URI를 꺼내 URI 객체로 빌드했다 . 

 

인터셉터가 스프링컨텍스트안에서 작동하기 떄문에 controller에 Handler에 대한 정보등을 사용할 수 있다. checkAnnotation 메서드에서 해당 요청의 핸들러가 @Auth를 갖고있는지 HandlerMethod를 통해 체크한 후  갖고있다면 if문에서 검증절차를 거쳐 검증에 성공할 경우 true를 리턴하여 Controller 단으로 넘어가고 아닐 false 일 경우 컨트롤러로 넘어가지 못하고 리턴된다 ,  

통신이 실패하는 것은 아니어서 요청상태는 200이 뜨기 떄문에 Exception을 터트려 ExceptionHandler에서 ResponseEntity에 401(권한 없음)을 지정하여 리턴한다.

 

해당 어노테이션은 컨트롤러 전체에 붙힐수도 있지만 ,특정 핸들러에  직접 붙혀 특정 경로에서만 인터셉터를 타게 만들 수도 있다. 

 또는 아래의 Config 클래스에 인터셉트를 등록하고 , addPathPatterns메서드에 경로를 지정해도 특정 경로만 인터셉트를 타게 할 수 있다.

 

인터셉터를 사용하기 위해선 등록이필요하다 보통 MvcConfig, WebConfig 같은 이름으로 Config클래스를 만들어 등록한다 . 

 

 WebConfig

 

package com.example.hello.interceptorTest.config;

import com.example.hello.interceptorTest.interceptor.AuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // '/api/private' 이하의 모든 주소에서 해당 인터셉터를 타게 한다.
        registry.addInterceptor(authInterceptor).addPathPatterns("/api/private/*");
    }
}

/api/private 하위의 모든경로만 interceptor를 탈것이다. 

 

어노테이션을 만들고 어노테이션이 있느지 확인하고 하는 것보단 위의 addPathPattern메서드로 지정하는 것이 편할것 같다. 

 

아, 여기까지 써놨는데 , 다  날라갔다 티스토리 주겨버.. 

 

ApiTester로 테스트 해보자

 

 

/api/private 경로로 잘못된 값을 보냈더니 인터셉터를 탔고 검증 부분에서 false가 떴고 예외가 터졌다. 

 

/api/public 경로는 인터셉터를 거치지 않았고 검증 없이 요청 응답이 성공했다.  

반응형