본문 바로가기
Spring Boot

Spring Boot - Interceptor

by sinabeuro 2021. 9. 23.
728x90

 

Interceptor

Interceptor란 fIlter와 매우 유사한 형태로 존재 하지만, 차이점은 Spring Context에 등록 됩니다.

AOP와 유사한 기능을 제공할 수 있으며, 주로 인증 단계를 처리하거나 Logging를 하는데 사용합니다.

이를 선/후 처리함으로써, Service business logic과 분리 시킵니다.

Client에서 요청을 하면 다음과 같은 순서로 실행됩니다.

Filter -> Interceptor -> AOP -> Controller

 


 

Interceptor 예제 - 어노테이션으로 권한 체크

 

@RestController
@RequestMapping("/api/private")
@Auth
@Slf4j
public class PrivateController {

    @GetMapping("/hello")
    public String hello() {
        log.info("private hello controller");
        return "private hello";
    }
}

@Auth 어노테이션은 Interceptor를 설정할 사용자 정의 어노테이션입니다.

컨트롤러는 위와 같이 간단하게 만들어주시구요.

 

// Auth.java 사용자 어노테이션
package com.example.interceptor.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Auth {
}

사용자 정의 @Auth 어노테이션도 간략하게 구현합니다.

 

package com.example.interceptor.config;

import com.example.interceptor.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       // final로 선언된 객체들을 생성자에서 주입 받을 수 있도록 해줌
public class MvcConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;
    // MvcConfig(AuthInterceptor) 생성자가 자동으로 생김
    // @Autowired 로 주입받을 수 있지만, 순환 참조가 일어날 수 있기 때문에 순환참조를 막기 위해 생성자를 통해서 주입 받는다.
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //WebMvcConfigurer.super.addInterceptors(registry);
        registry.addInterceptor(authInterceptor);
    }
}

 

AuthInterceptor 클래스는 우리가 만들 Interceptor를 실행하는 비지니스 로직입니다.

AuthInterceptor 클래스를 @RequiredArgsConstructor 어노테이션을 통해서 final로 주입받습니다.

(사실 @Autowired로 주입받아도 크게 상관은 없지만, 로직의 규모가 커지면 @RequiredArgsConstructor 어노테이션으로 주입받는 것이 좋을 것 같습니다.)

 

WebMvcConfigurer 를 상속받고 addInterceptors 메소드를 오버라이드해줍니다.

addInterceptors 메소드에서 registry 로 Interceptor 클래스를 등록하면 Interceptor가 실행됩니다.

 

실행될 Interceptor 는 다음과 같습니다.

package com.example.interceptor.interceptor;

import com.example.interceptor.annotation.Auth;
import com.example.interceptor.exceptiom.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
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // filter에서 만들어진 ContentCachingRequestWrapper와 ContentCachingResponseWrapper를 만들어
        // FilterChain.doFilter(ContentCachingRequestWrapper, ContentCachingResponseWrapper)에 넣어준 값을 형변환 해줄 수 있습니다.

        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 );
        // controller에 붙인 Auth 어노테이션으로 접근 권한을 정한다.

        // 나의 서버는 모두 public 으로 동작을 하는데
        // 단! Auth 권한을 가징 요청에 대해서는 세션, 쿠키
        // 이 방식은 어노테이션을 가지고 체크하는 방식입니다.
        if(hasAnnotation) {
            // 권한 체크
            String query = uri.getQuery();          // 보통 세션으로 권한을 체크한다.
            log.info("query : {}", query);
            if(query.equals("name=steve")) {
                return true;
            }
            throw new AuthException("steve가 아님");
        }

        return true;
        // true 값이여야 interceptor에서 controller로 넘어간다.
    }

    private boolean checkAnnotation(Object handler, Class clazz) {

        // resourcer javascript, html
        // instanceof 형변환 여부를 파악한다.
        if( handler instanceof ResourceHttpRequestHandler) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;

        System.out.println(handlerMethod.getMethodAnnotation(clazz));
        System.out.println(handlerMethod.getBeanType().getAnnotation(clazz));
        if(null != handlerMethod.getMethodAnnotation(clazz) || null != handlerMethod.getBeanType().getAnnotation(clazz)) {
            return true;
        }

        return false;

    }
}

인터페이스 클래스에 AuthInterceptor를 상속받고 preHandle 메소드를 오버라이드합니다.

checkAnnotation 메소드를 통해서 정의한 @Auth 어노테이션이 요청된 request의 컨트롤러에 존재하는지 여부를 체크합니다. 

@Auth 어노테이션이 존재하면, hasAnnotation의 변수가 true임으로 권한체크를 진행합니다.

반면, @Auth 어노테이션이 존재하지않으면, hasAnnotation의 변수가 false임으로 권한체크를 하지않습니다.

 

마지막으로 preHandle 메소드의 return 값이 true이면, controller에서 정상적인 response를 출력합니다.

반면, preHandle 메소드의 return 값이 false이면, reponse 상태값은 200으로 정상이지만, controller를 타지 않습니다.

 

 

AuthException의 정의(참고만)

package com.example.interceptor.exceptiom;

public class AuthException extends RuntimeException {

    private final String message;

    public AuthException(String message) {
        this.message = message;
    }

    public AuthException(String message, String message1) {
        super(message);
        this.message = message1;
    }

    @Override
    public String getMessage() {
        //return super.getMessage();
        return this.message;
    }
}

RuntimeException 상속받고, 생성자를 만들고, getMessage를 오버라이딩하여 간략하게 구현하시면 됩니다.

 

package com.example.interceptor.handler;

import com.example.interceptor.exceptiom.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(AuthException e) {
        System.out.println(e.getMessage());
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
    }
}

 

Interceptor 예제 - URI로 Interceptor 적용

 

package com.example.interceptor.config;

import com.example.interceptor.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       // final로 선언된 객체들을 생성자에서 주입 받을 수 있도록 해줌
public class MvcConfig implements WebMvcConfigurer {

    private final AuthInterceptor authInterceptor;
    // MvcConfig(AuthInterceptor) 생성자가 자동으로 생김
    // @Autowired 로 주입받을 수 있지만, 순환 참조가 일어날 수 있기 때문에 순환참조를 막기 위해 생성자를 통해서 주입 받는다.
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //WebMvcConfigurer.super.addInterceptors(registry);
        registry.addInterceptor(authInterceptor)
            .addPathPatterns("/api/private/*");

        // registry 에 등록된 순서대로 인터셉터가 동작함으로, 권한 체크도 여러 뎁스로 사용이 가능하다.
    }
}

.addPathPatterns("/api/private/*"); 와 같이 

registry.addInterceptor(...).addPathPatterns() 메소드에 Interceptor를 적용하고 싶은 request URL를 기입합니다.

이와 같은 방법으로 해당 URL에 매핑된 컨트롤러에서 Interceptor를 적용할 수 있습니다.

 

이상 Interceptor를 어노테이션으로 적용하는 방법과 InterceptorRegistry에 URL을 등록하는 방식을 알아보았습니다.

 

 

 

 

 

 

728x90

'Spring Boot' 카테고리의 다른 글

Spring Boot - 서버 간 통신 예제  (0) 2021.09.26
Spring Boot - 서버 간 통신 RestTemplate 정의  (0) 2021.09.25
Spring Boot - Exception 구현 예제  (0) 2021.09.13
Spring Boot - Filter  (0) 2021.09.12
Spring Boot - Exception 개괄  (0) 2021.09.10

댓글