[Dining-together] msa 환경에서 회원 권한체크를 위한 interceptor && 토큰 검증을 위한 gateway Custom Filter - interceptor, filter, AOP 차이

Jifrozen·2021년 7월 11일
1

Dining-together

목록 보기
15/25

msa 환경에서 회원 권한체크를 위한 interceptor && 토큰 검증을 위한 gateway Custom Filter

interceptor, filter, AOP 차이

공통으로 처리해야 할 업무들에 대한 고민 (공통 프로세스)
예를 들어 로그인 관련 처리, 권한체크, 로그, 인코딩, 예외처리 등이 존재한다.

공통으로 처리해야할 업무를 모든 로직에 작성하면 중복코드가 많아지고 부하가 커진다.

공통 처리를 위해 활용할 수 잇는것은 3가지
1. Filter
2. Interceptor
3. AOP

3가지 모두 어떤 로직 이전 이후에 추가적인 행동을 실행할 수 있다.
그렇다면 3가지의 차이점이 뭔지 그리고 내가 프로젝트 권한체크를 위해 어떤걸 이용해야할지 알아보도록 하겠다.

  1. 서버를 실행시켜 서블릿이 올라오는 동안에 init이 실행되고, 그 후 doFilter가 실행된다.

  2. 컨트롤러에 들어가기 전 preHandler가 실행된다

  3. 컨트롤러에서 나와 postHandler, after Completion, doFilter 순으로 진행이 된다.

  4. 서블릿 종료 시 destroy가 실행된다.

1. Filter

말그대로 요청과 응답을 거른 뒤 정제하는 역할

DispatcherServlet 이전에 실행이 되는데 필터가 동작하도록 지정된 자원의 앞단에서 요청내용을 변경하거나, 여러가지 체크를 수행 할 수 있다.

또한 자원의 처리가 끝난 후 응답내용에 대해서도 변경하는 처리를 할 수 있다.
일반적으로 인코딩 변환, XSS방어 등의 요청에 대한 처리로 사용

[ 필터의 실행메서드 ]

  • init() - 필터 인스턴스 초기화

  • doFilter() - 전/후 처리

  • destroy() - 필터 인스턴스 종료

2. interceptor

요처에 대한 작업 전/후로 가로채는 역할

필터는 컨텍스트 외부에 존재하여 스프링과 무관한 자원에 대해 동작 하지만 인터셉터는 스프링의 DispatcherServlet 이 컨트롤러를 호출 하기 전, 후로 끼어들기 때문에 컨텍스트 내부에서 Controller에 관한 요청과 응답에 대해 처리한다.

보통 로그인 체크, 권한체크, 프로그램 실행시간 계산작업, 로그확인 등을 처리한다.

[인터셉터의 실행메서드]

  • preHandler() - 컨트롤러 메서드가 실행되기 전

  • postHanler() - 컨트롤러 메서드 실행직 후 view페이지 렌더링 되기 전

  • afterCompletion() - view페이지가 렌더링 되고 난 후

3. AOP

OPP를 보완하기 위해 나온 개념

객체 지향의 프로그래밍을 했을 때 중복을 줄일 수 없는 부분을 줄이기 위해 종단면(관점)에서 바라보고 처리한다.

주로 '로깅', '트랜잭션', '에러 처리'등 비즈니스단의 메서드에서 조금 더 세밀하게 조정하고 싶을 때 사용합니다.

Interceptor나 Filter와는 달리 메소드 전후의 지점에 자유롭게 설정이 가능하다.

Interceptor와 Filter는 주소로 대상을 구분해서 걸러내야하는 반면, AOP는 주소, 파라미터, 애노테이션 등 다양한 방법으로 대상을 지정할 수 있다.

특히 파라미터의 경우 advice와 handlerInterceptor 와의 가장 큰 차이점이다.

그럼 프로젝트에 뭘 이용해야할까?

현재 우리 서비스에는 업체와 고객 운영자 권한이 존재한다. 그리고 업체만 할수잇는 기능 고객만 할 수 잇는 기능이 있기 때문에 권한체크를 필수적으로 진행해야한다.
msa환경 에서 인증 서버와 다른 서버에서 권한체크를 어떻게 할 수 있을까? 토큰은 어떻게 검증해야하나? 고민을 많이했다.

이전 프로젝트는 preAuthorize postAuthorize와 같은 스프링 시큐리티 어노테이션을 이용하거나 토큰의 경우 하나의 서비스안에서 검증하면 됐는데....

고민하고 시도했던 방법들을 나열해보겠다.
1. gateway에 Custom Filter를 만들어 request body에 잇는 권한과 토큰을 검증한다. -> 권한은 불가능 filter는 request body를 가져올 수 없다. 대신 header 는 가져와 토큰 유효성 검사 가능!
2. 토큰 payload에 권한(업체, 고객, 운영자) 넣어 토큰을 풀어 체크한다. -> 토큰 decode 코드가 없었다....
3. feign client 로 user 권한을 인증 서버에서 가지고와서 Interceptor와 어노테이션으로 권한체크를 한다 -> 해결

import com.google.common.net.HttpHeaders;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
	Environment env;

	public AuthorizationHeaderFilter(Environment env) {
		super(Config.class);
		this.env = env;
	}

	public static class Config {
		// Put configuration properties here
	}

	@Override
	public GatewayFilter apply(Config config) {
		return (exchange, chain) -> {
			ServerHttpRequest request = exchange.getRequest();

			if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
				return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
			}

			String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
			String jwt = authorizationHeader;

			if (!isJwtValid(jwt)) {
				return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
			}

			return chain.filter(exchange);
		};
	}

	private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
		ServerHttpResponse response = exchange.getResponse();
		response.setStatusCode(httpStatus);

		log.error(err);
		return response.setComplete();
	}

	private boolean isJwtValid(String jwt) {
		boolean returnValue = true;

		String subject = null;

		try {
			subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
				.parseClaimsJws(jwt).getBody()
				.getSubject();
		} catch (Exception ex) {
			returnValue = false;
		}

		if (subject == null || subject.isEmpty()) {
			returnValue = false;
		}

		return returnValue;
	}

}
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: member
          uri: lb://MEMBER
          predicates:
            - Path=/member/auth/**
  1. feign client 로 user 권한을 인증 서버에서 가지고와서 Interceptor와 어노테이션으로 권한체크를 한다 -> 해결
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
	String target() default "CUSTOMER";
}
@RequiredArgsConstructor
@Component
public class PermissionInterceptor implements HandlerInterceptor {

	private final UserServiceClient userServiceClient;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws
		Exception {

		HandlerMethod method = (HandlerMethod)handler;
		Permission permission = method.getMethodAnnotation(Permission.class);

		if ((!(handler instanceof HandlerMethod)) || (permission == null)) { // 호출되는 메소드가 헨들러가 아니라면 검증할 필요가 없겠죠?
			return true;
		}
		String token = request.getHeader("X-AUTH-TOKEN");
		if (token == null) {
			throw new UserNotFoundException();
		}
		UserIdDto userIdDto = userServiceClient.getUserId(token);
		if (!permission.target().equals(userIdDto.getType())) {
			throw new UnAuthorizedException();
		}

		return true;
	}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {

	private PermissionInterceptor permissionInterceptor;

	@Autowired
	public WebConfig(@Lazy PermissionInterceptor permissionInterceptor) {
		this.permissionInterceptor = permissionInterceptor;
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(permissionInterceptor)
			.addPathPatterns("/auction/**");
	}
}

참고문서

https://goddaehee.tistory.com/154 [갓대희의 작은공간]
https://ecsimsw.tistory.com/entry/Spring-Interceptor-권한-확인-어노테이션
https://velog.io/@kyle/회원에-권한은-어떻게-관리할까

0개의 댓글