YoungSoo

[Spring Cloud Gateway] MSA를 향해 가는 API Gateway 도입기 - 2 본문

BE/Spring

[Spring Cloud Gateway] MSA를 향해 가는 API Gateway 도입기 - 2

YoungSooSoo 2023. 5. 30. 23:06

일단 먼저 API Gateway 애플리케이션을 위해 프로젝트를 만들어줍니다.

Spring Cloud Gateway와 Spring Security로 API Gateway 구현

 

API Gateway 애플리케이션을 만들기 위한 의존성 설정(필요한 의존성 들은 후에 주입할 예정)

 

Spring Cloud Gateway는 Spring WebFlux를 기반으로 동작하는 서비스입니다. 따라서 Spring Cloud Gateway와 Spring Security를 함께 사용하기 위해서는 Spring WebFlux도 함께 사용해야 합니다.

 

Spring WebFlux는 비동기 및 반응형 웹 애플리케이션을 개발하기 위한 기능을 제공하는 모듈입니다. 이 모듈은 Reactive Streams를 기반으로 하며, 넌블로킹 I/O 모델을 사용하여 더 효율적인 웹 애플리케이션 개발을 가능하게 합니다.

 

Spring Cloud Gateway는 Spring WebFlux를 사용하여 동작하기 때문에, Spring Security와 함께 사용하기 위해서는 Spring WebFlux도 의존성으로 추가해야 합니다. 이를 통해 Gateway와 Security를 통합하여 안전하고 확장 가능한 마이크로서비스 아키텍처를 구축할 수 있습니다.

 

현재 생각하고 있는 방식은 아래와 같은 방식을 통해 서비스의 인증/인가를 처리해주려고 합니다.

  1. 기존의 핸들러에서 액세스 토큰을 쿠키에 저장하는 부분을 제거합니다.
  2. Gateway에 새로운 라우트를 추가하여 "/member/login" 엔드포인트에 대한 요청을 처리하도록 합니다. 이 라우트는 실제 인증을 처리하는 서비스로 요청을 전달합니다.
  3. 클라이언트는 인증이 필요한 리소스에 요청을 보냅니다. 요청은 Spring Cloud Gateway로 진입합니다.
  4. Gateway에서는 "/member/login" 요청을 받아 인증 서비스에 전달합니다. 이때, Spring Security의 필터 체인이 동작하게 됩니다.
  5. Spring Security 필터 체인에서 JWT 인증을 처리하기 위해 필터가 실행됩니다. 일반적으로 AuthenticationFilter나 JwtAuthenticationFilter와 같은 커스텀 필터를 구현하여 JWT 인증을 처리합니다.
  6. JWT 인증 필터는 요청에서 헤더 또는 쿠키 등을 통해 JWT 토큰을 추출합니다.
  7. 추출한 JWT 토큰의 유효성을 검사합니다. 유효성 검사는 JWT 토큰의 서명 검증, 만료 여부 확인, 필요한 권한 등의 검사를 포함합니다.
  8. JWT 토큰이 유효하다면, 토큰에서 사용자 정보나 권한 등 필요한 정보를 추출하여 인증 객체를 생성합니다.
  9. 인증 객체를 Spring Security의 SecurityContextHolder에 저장하여 해당 요청에 대한 인증 정보를 유지합니다.
  10. Spring Security 필터 체인은 다음 필터 또는 리소스로 요청을 전달합니다. 인증이 필요한 리소스에 접근할 때는 인증 정보를 확인하여 접근 권한을 결정합니다.
  11. 요청이 리소스 서비스로 전달되어 해당 리소스를 처리합니다.

 

이때, Spring Cloud Gateway에서 Spring Security를 사용하려면 Spring WebFlux를 함께 사용해야 합니다. Spring Cloud Gateway는 WebFlux 기반으로 구축되었으며, WebFlux는 비동기적인 요청-응답 처리를 지원하는 리액티브 스택입니다. 따라서 Spring Security를 Spring Cloud Gateway에서 사용하기 위해서는 WebFlux를 사용하여 비동기적인 방식으로 요청을 처리해야 합니다.

RouteLocator

// RouteLocator를 사용하여 경로별로 라우팅 설정을 정의합니다.
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        // Member 서비스로의 요청을 member-service로 라우팅합니다.
        .route("member-service", r -> r.path("/member/**")
            .filters(f -> f.rewritePath("/member/(?<path>.*)", "/member/${path}"))
            .uri("http://localhost:8081"))
        // OAuth2 로그인 관련 요청을 oauth2-login-service로 라우팅합니다.
        .route("oauth2-login-service", r -> r
            .path("/oauth2/**")
            .filters(f -> f.rewritePath("/oauth2/(?<path>.*)", "/oauth2/${path}"))
            .uri("http://localhost:8081"))
        // 카카오 로그인을 위한 라우팅 설정
        .route("kakao-login", r -> r
            .path("/login/oauth2/code/kakao/**") // 수정된 경로 설정
            .filters(f -> f.rewritePath("/login/oauth2/code/kakao/(?<path>.*)", "/oauth2/code/kakao/${path}")) // 수정된 리라이팅 설정
            .uri("http://localhost:8081"))
        // product-service로의 요청을 라우팅합니다.
        .route("product-service", r -> r.path("/product/**")
            .filters(f -> f.filter(jwtAuthenticationFilter()).rewritePath("/product/(?<path>.*)", "/product/${path}"))
            .uri("lb://localhost:8080/"))
        // reviews-service로의 요청을 라우팅합니다. 
        .route("reviews-service", r -> r.path("/reviews/**")
            .filters(f -> f.filter(jwtAuthenticationFilter()).rewritePath("/reviews/(?<path>.*)", "/reviews/${path}"))
            .uri("lb://localhost:8080/"))
        // basket-service로의 요청을 라우팅합니다.
        .route("basket-service", r -> r.path("/basket/**")
            .filters(f -> f.filter(jwtAuthenticationFilter()).rewritePath("/basket/(?<path>.*)", "/basket/${path}"))
            .uri("lb://localhost:8080/"))
        .build();
}

먼저 각 마이크로 서비스로의 라우팅을 위해 자바 코드를 통해  정의해주었습니다.

이 부분은 자바코드가 아닌 yaml 파일로도 정의가 가능하지만 저는 자바 코드를 통해 정의해주었습니다.

GatewayFilter

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

        // 쿠키에서 토큰 추출
        String token = extractTokenFromCookie(headers);

        log.info("Access Token: " + token);

        if (!StringUtils.isEmpty(token)) {
            ServerHttpRequest modifiedRequest = request.mutate()
                .header(HttpHeaders.COOKIE, "access_token=" + token)
                .build();
            ServerWebExchange modifiedExchange = exchange.mutate().request(modifiedRequest).build();
            return chain.filter(modifiedExchange);
        } else {
            // 토큰 추출 실패 시 적절한 예외 처리 또는 로직 수행
            return Mono.error(new RuntimeException("Failed to extract token from cookie."));
        }
    };
}

클라이언트가 API Gateway로 요청을 보낼 때 쿠키에 포함된 액세스 토큰을 추출하여 수정된 요청을 전달하게 됩니다. 이를 통해 API Gateway는 수정된 요청을 다음 필터 또는 서비스로 전달합니다.

 

Restful API

@GetMapping("/isvalid")
    public boolean isValid(@CookieValue(name = "access_token", required = false) String accessToken) {
        
        // 유효성 검사
        return jwtService.validateToken(accessToken);
    }

 

저는 일단 Restful API를 통해 토큰의 유효성 검사를 진행해주었습니다. 토큰이 유효하지 않을 시에 요청을 보낸 서비스에서 토큰을 재발급을 요청하려고 합니다.

 

 

다음 글에서는 메인 서비스와 사용자 서비스를 나누고 토큰 발급과 인증 관련된 부분을 진행하겠습니다.