[MSA구축] Spring Cloud API-Gateway 의 인증인가처리(1)

S-J LEE·2023년 3월 20일
2

0. 개요

Spring Cloud API Gateway를 MSA에서 도입하는 이유는 여러 가지가 있다. 먼저, API Gateway는 요청을 중앙 집중화하여 처리함으로써 마이크로서비스 간 통신을 단순화한다. 또한, 라우팅, 로드 밸런싱, 인증 및 인가와 같은 공통 기능을 한 곳에서 처리할 수 있어 관리 효율성이 증가한다. 그리고, 인프라스트럭처를 확장할 때 유연성을 제공함으로써 시스템이 성장해도 원활한 처리가 가능하다.


1. 기술의 채택 이유

Spring Cloud API Gateway와 비교하여, Spring Cloud Zuul은 현재 유지 보수 모드에 있어서 더 이상의 주요 기능 개선이 이루어지지 않는다. 대신 Spring Cloud Gateway가 계속해서 업데이트되고 있으며, 비동기식 아키텍처와 더 나은 성능을 제공하고 있다. 또한, Spring Cloud Gateway는 Netty 기반의 비동기 네트워킹 프레임워크를 사용하여 고성능과 확장성을 갖추고 있다.

이러한 이유로 현재 MSA에서는 Spring Cloud API Gateway를 도입하는 것이 Spring Cloud Zuul보다 월등하다고 판단하여 채택하게 되었다.


2. 설계 방향

API-GATEWAY SERVICE에서 MICROSERVICE로의 통신 과정에서, 인증 인가과정을 통합적으로 관리토록 설계 하였다.
PREDICATE의 값에 따라 필터값의 적용을 달리 하는 형태로 인증/인가 문제를 해결하였다.

2-1. 인증 인가 Filter 구축(1)

인증/인가의 가장 까다로운 문제는, 인증/인가의 상태를 여러 MICROSERVICE에서 전역적으로 관리 되어야 한다는 제한 조건을 준수하는 것이다. 예컨대 MAIN-SERVICE의 POSTS의 GET 요청의 경우, 인증인가에 따라 보여지는 값이 달라지게 된다. 로그인 한 사용자의 경우 특정 글에 대한 '나의 북마크'를 확인할 수 있어야 하기 때문이다.

그러한 관점에서, Method의 단위로, 인증인가를 필터링 할 수만은 없다는 제한이 걸리게 된다. 그래서 인증인가의 유무 및 정보를 헤더값으로 만들어서 보내주는 형태의 설계로 이 문제를 해결하였다.

<AuthFilter.java>

@Slf4j
@Component
public class AuthFilter extends AbstractGatewayFilterFactory<AuthFilter.Config> {

    private final JwtUtil jwtUtil;

    @Autowired
    public AuthFilter(JwtUtil jwtUtil) {
        super(AuthFilter.Config.class);
        this.jwtUtil = jwtUtil;
    }

    public static class Config {
        //config
    }

    /*토큰 검증 필터*/
    @Override
    public GatewayFilter apply(AuthFilter.Config config) {
        return (exchange, chain) -> {
            //ServerHttpRequest
            ServerHttpRequest request = exchange.getRequest();

            List<String> accessToken = jwtUtil.getHeaderToken(request, "Access");
            List<String> refreshToken = jwtUtil.getHeaderToken(request, "Refresh");


            if (accessToken != null && jwtUtil.tokenValidation(accessToken.get(0))) {
                request.mutate().header("Auth", "true").build();
                request.mutate().header("Account-Value", jwtUtil.getEmailFromToken(accessToken.get(0))).build();
                return chain.filter(exchange);
            }

            request.mutate().header("Auth", "false").build();
            return chain.filter(exchange);
        };
    }
}

2-1. 인증 인가 Filter 구축(2)

인증 인가 Filter 구축(1)을 통해, 인증/인가를 전역적으로 관리하는데는 성공하였다. 하지만, 반드시 인증인가가 필요한 predicate의 경우에는 api-gateway에서 msa와 통신하지 않고 차단하게 된다면 조금 더 효율적인 설계라고 할 수 있다.

이를 해소 하기 위해 AuthPermissionFilter를 만들어, 인증인가정보가 존재하지 않는경우 Api-gateway단계에서 바로 에러를 반환하는 로직을 구현 하였다.

<AuthPermissionFilter.java>

@Slf4j
@Component
public class AuthPermissionFilter extends AbstractGatewayFilterFactory<AuthPermissionFilter.Config> {

    private final JwtUtil jwtUtil;

    @Autowired
    public AuthPermissionFilter(JwtUtil jwtUtil) {
        super(AuthPermissionFilter.Config.class);
        this.jwtUtil = jwtUtil;
    }

    public static class Config {
        //config
    }

    /*토큰 검증 필터*/
    @Override
    public GatewayFilter apply(AuthPermissionFilter.Config config) {
        return (exchange, chain) -> {
            //ServerHttpRequest
            ServerHttpRequest request = exchange.getRequest();

            List<String> accessToken = jwtUtil.getHeaderToken(request, "Access");
            List<String> refreshToken = jwtUtil.getHeaderToken(request, "Refresh");

            /*토큰 x or 검증 -> error 반환*/
            if (accessToken == null) {
                return jwtUtil.onError(exchange, "No Access token", HttpStatus.UNAUTHORIZED);
            }
            if (!jwtUtil.tokenValidation(accessToken.get(0))) {
                return jwtUtil.onError(exchange, "AccessToken is not Valid", HttpStatus.UNAUTHORIZED);
            }

            /*정상 토큰이 존재하는 경우*/
            request.mutate().header("Auth", "true").build();
            request.mutate().header("Account-Value", jwtUtil.getEmailFromToken(accessToken.get(0))).build();
            return chain.filter(exchange);

        };
    }
}

검증 및 에러 로직

Webflux의 경우에는 servlet이 아닌 ServerHttpResponse를 사용한다는 특징이 있다.

Application.yml 설정

spring:
  cloud:
    gateway:
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials
      routes:
        # Main-service
        - id: main-service
          uri: lb://MAIN-SERVICE
          predicates:
            - Path=/main-service/**
          filters:
            - AuthFilter
        # Main-service Auth filtering
        - id: main-service
          uri: lb://MAIN-SERVICE
          predicates:
            - Path=/main-service/applyments/**, /main-service/likes/**, /main-service/notifications/**, /main-service/posts
            - Method=PATCH,DELETE,POST
          filters:
            - AuthPermissionFilter
        # Chat-Service
        - id: chat-service
          uri: lb://CHAT-SERVICE
          predicates:
            - Path=/chat-service/**
            #            - Method=GET
          filters:
            - AuthFilter
            #              - RemoveRequestHeader=Cookie
        # Chat-service 인증인가 filtering
        - id: chat-service
          uri: lb://CHAT-SERVICE
          predicates:
            - Path=/chat-service/chat/**
            - Method=POST
          filters:
            - AuthPermissionFilter

두 개의 필터를 사용하므로써 인증/인가의 허용 범위를 좀 더 명확하게 세팅 할 수 있었다. 특히 많은 전문가들이 api-gateway를 통해 인증/인가를 할 경우 Predicate에 따라 명확하게 권한부여가 어렵다는 단점을 꼽고 있는데, 이를 단번에 해소하였다


3. Trouble Shooting

3.1. Webflux의 CORS 설정

Spring MVC와 달리 CORS의 설정도 약간은 다른면이 있다.
공식문서에서 application.properties에 설정 추가만으로 해결된다고 하였지만, 막상 정상적으로 적용되지 않는 문제도 있었다.

3.1.1. CORS 설정이 적용되지 않는 오류

앞서 말했듯이 공식문서에 나와있는 application.properties를 통해 cors설정을 하는 방법은 왜인지 정상적으로 적용되지 않았다.

그래서 Spring MVC Stack을 사용할때처럼 CORS Configuration을 적용하였지만, 런타임 오류가 발생하였다.


일반적으로 MVC스택에서 사용하던 WebMvcConfigurer.
하지만, Spring Cloud Api-gate는 비동기방식인 Webflux Stack이기 때문에 mvcConfigurer를 사용 할 수 없는 사실을 알게 되었다.


숱하게 클래스를 타고 들어가서, 방법을 알아내었다.
WebFluxConfigurer를 구현하는 클래스를 생성한후, 세팅 해주었다.

3.1.2. API-GATEWAY이기 때문에 생기는 CORS 오류

모놀리식 아키텍쳐의 경우 외부의 요청을 단 한번 전달하게 된다. 그 과정에서 header의 값 또한 한번만 전달이 된다. 그런데, apigateway와 microservice 간의 통신을 하는 시점에 동일한 헤더값이, 여러번 중첩되어 전달 되는 현상이 생긴다. 여러번이 중첩되어 내보내지게 되면, CORS에러를 생성 하는 것으로 확인되었다.

이를 위한 세팅이 필요하다.

spring.cloud.gateway.default-filters 옵션을 통해, 기본적으로 모든 필터에 중복되는 헤더값을 제거 해주는 옵션을 세팅 해주어야 한다.

profile
MSA 와 관련된 기반 기술에 관심이 많습니다.

2개의 댓글

comment-user-thumbnail
2023년 3월 27일

Spring Cloud API-Gateway의 존재만 막연하게 알고 있었는데
상세한 코드가 있으니까 훨씬 와닿습니다!!

답글 달기
comment-user-thumbnail
2023년 3월 27일

개요부터 채택 이유 및 설계 그리고 마지막에 트러블 슈팅까지 작성하신 글의 흐름이 매끄러워, 읽는 데 아주 매끄러웠습니다. 또한 내용이 알차서 재밌게 읽었습니다. MSA통해 인증/인가 처리를 하게 될 때 참고하도록 하겠습니다!!

답글 달기