본문 바로가기
Java & Kotlin/Spring

Request Rate Limiting with Spring Cloud Gateway - 1. 기본 프로젝트 생성

by devson 2022. 1. 30.

전체 코드는 여기에서 확인 가능하다.


 

예제는 기존 시스템 구성에 Request Rate Limiting 기능을 추가하는 식으로 진행할 것이다.

그러기 위해서는 앞서 말한 기존 시스템을 구성할 것이다.

 

기존 시스템의 시퀀스 다이어그램

어느정도 현실성 있는 예제를 위해 API Gateway에서 인증을 확인하는 기능을 추가하였다.

Request Rate Limiting 기능은 이 바탕으로 추가할 것이다.

 

API 서버

먼저 API 서버를 생성해보자.

프로젝트 생성

Spring Web dependency만 추가한 SpringBoot 프로젝트를 생성한다.

Controller 추가

그리고 요청을 처리하기 위해 Controller를 추가한다.

이 API 서버는 단순하게 하나의 endpoint를 통해 모든 GET 요청을 처리하도록 처리하였다.

 

package com.tistory.devs0n.api

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletRequest

@SpringBootApplication
@RestController
class ApiServerApplication {
    @GetMapping("**")
    fun root(request: HttpServletRequest): Map<String, Any> {
        val userId = request.getHeader("X-USER-ID")

        println("Request from '${userId}' came in!")

        return mapOf(
            "success" to true,
            "message" to "Hello $userId",
        )
    }
}

fun main(args: Array<String>) {
    runApplication<ApiServerApplication>(*args)
}

 

요청으로부터 User ID에 해당하는 Header(X-USER-ID)를 조회 후 정해진 포맷의 응답을 하는 단순한 API 이다.

 

API 서버는 이것이 끝이다.

다음으로 이 API 서버 앞단에 위치할 API Gateway 서버를 만들어보자.

 

API Gateway 서버

프로젝트 생성

API Gateway 서버는 Spring Cloud Routing > Gateway dependency를 추가하여 생성하도록 한다.

 

인증 필터 추가

앞서 봤던 시퀀스 다이어그램에서 API Gateway에서 처리해야할 인증 기능에 대한 필터를 추가해보자.

Gateway 서버의 인증 필터

  1. 요청의 Header를 통해 인증 토큰을 받고
  2. 해당 인증 토큰에 해당하는 User ID를 추출하여
  3. 요청 Header에 User ID를 더하는 기능을 한다.

 

아래 코드는 AbstractGatewayFilterFactory를 상속받아 위 기능을 구현하는 custom filter를 생성한 것이다.

import org.springframework.cloud.gateway.filter.GatewayFilter
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.stereotype.Component
import reactor.core.publisher.Mono

@Component
class AuthFilter : AbstractGatewayFilterFactory<AuthFilter.Config>(Config::class.java) {
    data class Config(
        val whiteList: Map<String, String>, // key: 인증 토큰, key: User ID
    )

    override fun apply(config: Config): GatewayFilter {
        return GatewayFilter { exchange, chain ->
            val request = exchange.request
            val response = exchange.response
            val whiteList = config.whiteList

            if (this.hasValidAuthToken(request, whiteList)) {
                val userId = this.extractUserIdFromRequest(request, whiteList)

                return@GatewayFilter chain.filter(
                    exchange.mutate()
                        .request(
                            request.mutate()
                                .header("X-USER-ID", userId)
                                .build()
                        )
                        .build()
                )
            }

            return@GatewayFilter this.responseInvalidAuthToken(response)
        }
    }

    private fun hasValidAuthToken(request: ServerHttpRequest, whiteList: Map<String, String>): Boolean {
        val authToken = request.headers.getFirst("X-AUTH-TOKEN")
        return authToken?.let { whiteList.containsKey(it) } == true
    }

    private fun extractUserIdFromRequest(request: ServerHttpRequest, whiteList: Map<String, String>): String {
        val authToken = request.headers.getFirst("X-AUTH-TOKEN")!!
        return whiteList[authToken]!!
    }

    private fun responseInvalidAuthToken(response: ServerHttpResponse): Mono<Void> {
        response.statusCode = HttpStatus.FORBIDDEN
        response.headers.contentType = MediaType.APPLICATION_JSON
        return response.writeWith(
            Mono.just(
                response.bufferFactory().wrap(
                    """{
                        |"success": false,
                        |"message":"Invalid Auth Token"
                    |}""".trimMargin().toByteArray()
                )
            )
        )
    }
}

인증 토큰 검증은 단순하게 Config를 통해 전달받은 white list를 통해 검증하도록 하였다.

 

설정 추가

인증 필터를 추가하였으니 이제 이 필터를 사용하도록 Spring Cloud Gateway 설정을 추가해주면 된다.

server:
  port: 9000

spring:
  cloud:
    gateway:
      default-filters:
        - name: AuthFilter
          args:
            whiteList:
              chrisToken: chris

      routes:
        - id: all
          uri: http://localhost:8080
          predicates:
            - Path=/**

편의를 위해 인증 필터는 default filter로 두었고, white list는 하나의 토큰만을 통과할 수 있도록 하였다.

또한 라우팅 설정은 하나의 라우터에서 모든 요청을 처리하도록 하였다.

 

요청 테스트

앞서 만든 API 서버와 API Gateway 서버와 함께 실행 시킨 뒤에 요청을 보내 인증 필터 기능이 잘 동작하는지를 확인해보자.

  • 유효한 인증 토큰을 포함한 요청

 

  • 유효하지 않은 인증 토큰을 포함한 요청

 

요청 결과 원하는 대로 인증 필터가 잘 동작하는 것을 확인할 수 있다.

 


 

이로써 Request Rate Limiting 기능을 추가하기 위한 기본 프로젝트를 완성하였다.

다음 포스팅에서 본격적으로 Request Rate Limiting 기능을 어떻게 추가할지에 대해 알아보도록 하겠다.

댓글