Architecture/MSA

Spring Cloud Gateway - Route Predicate & Gateway Filter Factory

체리필터 2020. 12. 28. 11:01
반응형

Gateway를 만들면서 사용할 수 있는 룰들을 셋팅하는데 있어서 구글링 및 공식 Document를 보는 것에 시간이 걸려 하나씩 정리해 둔다.

참고로 사용하게 되는 Predicate Factory는 org.springframework.cloud.gateway.handler.predicate 아래에 있다. ( github.com/spring-cloud/spring-cloud-gateway/tree/2.2.x/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/predicate )

    @Autowired
    private SecureHeadersProperties secureHeadersProperties;
    
    /**
     * ApiGateway Route Test Bean
     * @param builder
     * @param uriConfiguration
     * @return
     */
    @Bean
    public RouteLocator myRoutes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration) {
        String testUrl = uriConfiguration.getTest();
        String chauffeurUrl = uriConfiguration.getChauffeur();

        ZonedDateTime zonedDateTime = ZonedDateTime.parse("2020-12-28T10:42:00+09:00[Asia/Seoul]", DateTimeFormatter.ISO_DATE_TIME);

        // 특정 path로 들어올 경우 특정 header 값 추가
        RouteLocator headerAddRouteLocator = builder.routes().route(p -> p
                .path("/get", "/post")
                .and().method(HttpMethod.GET, HttpMethod.POST)  // 아래 methodRouteLocator 내용을 추가 적용...
                .filters(f -> f.addRequestHeader("Hello", "World"))
                .uri(testUrl)).build();

        // 특정 host header 값을 가지고 있는 경우 fallback uri 정의
        RouteLocator hostRouteLocator = builder.routes().route(p -> p
                .host("*.hystrix.com")
                .filters(f -> f.hystrix(config -> config
                        .setName("mycmd")
                        .setFallbackUri("forward:/fallback")
                ))
                .uri(testUrl)).build();

        // 특정 시간 이후에만 접속 가능하도록. 그 시간 이전에는 404 not found가 나옴
        RouteLocator afterRouteLocator = builder.routes().route(p -> p
                .after(zonedDateTime)
                .uri(testUrl)).build();

        // 특정 시간 이전에만 접속 가능하도록. 그 시간 이후에는 404 not found가 나옴
        RouteLocator beforeRouteLocator = builder.routes().route(p -> p
                .before(zonedDateTime).uri(testUrl))
                .build();

        // 특정 path와 특정 시간을 같이 쓰고 싶은 경우 and를 사용. 사용 방법 몰라 많이 헤맸음
        RouteLocator afterAndHeaderRouteLocator = builder.routes()
                .route(
                    p -> p
                    .path("/get").and().after(zonedDateTime)
                    .filters(f -> f.addRequestHeader("Hello", "World"))
                    .uri(testUrl)
                )
                .build();

        // mypage 면서 jwt 토큰이 없는 경우에는 무조건 팅겨 내게 만들 수 있다.
        RouteLocator cookieRouteLocator = builder.routes()
                .route(p -> p
                    .path("/mypage").and().cookie("jwt", "123")
                    .uri(testUrl)
                )
                .build();

        // 헤더 정보 체크. 없을 경우 404 not found
        RouteLocator headerCheckRouteLocator = builder.routes()
                .route(p -> p
                    .header("X-API-VERSION", "1.0.0")
                    .uri(chauffeurUrl)
                )
                .build();

        // 특정 메소드로만 호출하도록... 허용되지 않은 메소드로 호출 시 404 not found
        RouteLocator methodRouteLocator = builder.routes()
                .route(p -> p
                    .method(HttpMethod.GET, HttpMethod.POST)
                    .uri(testUrl)
                )
                .build();

        // 특정 쿼리스트링 파라미터의 값을 판별. 해당 이름의 해당 값이 없으면 404 not found
        RouteLocator queryRouteLocator = builder.routes()
                .route(p -> p
                    .query("red", "gree.")  // 두 번째 파라미터 regex가 없을 경우 해당 파라미터 키만 있어도 됨.
                    .uri(testUrl)
                )
                .build();

        // ip 목록에 있는 것만 허용인데 잘 안됨.
        RouteLocator RemoteAddrRouteLocator = builder.routes()
                .route(p -> p
                    .remoteAddr("192.168.1.1/16", "실제공인아이피주소", "localhost", "127.0.0.1")
                    .uri(testUrl)
                )
                .build();

        // Weight 값에 따라 % 비율로 할당. 간단한 로드밸러서 역할 가능할 듯... group 이름이 같아야 함.
        RouteLocator weightRouteLocator = builder.routes()
                .route(p -> p
                    .weight("group1", 8)
                    .uri("https://www.naver.com")
                )
                .route(p -> p
                    .weight("group1", 2)
                    .uri("https://www.daum.net")
                )
                .build();

        // 특정 파라미터를 추가하여 route, "/get" 으로 호출하면 red=get 으로 파라미터 전달.
        RouteLocator addRequestParamRouteLocator = builder.routes()
                .route(p -> p
                    .path("/{segment}")
                    .filters(f -> f.addRequestParameter("red", "{segment}"))
                    .uri(testUrl)
                )
                .build();

        // 특정 Response Header 값을 추가하여 리턴
        RouteLocator addResponseHeaderRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
                    .filters(f -> f.addResponseHeader("X-Response-Red", "Blue"))
                    .uri(testUrl)
                )
                .build();

        // dedupeResponseHeader method의 javadoc에는 다음처럼 이야기 함 "A filter that removes duplication on a response header before it is returned to the client by the Gateway"
        // 중복되는 헤더를 제거한다고 하는 듯. 첫 번째 파라미터는 헤더 네임인데 스페이스 구분자로 구분, strategy는 뭘 남겨둘지 정하는 듯.
        RouteLocator dedupeRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
                    .filters(f -> f
                        .addResponseHeader("Access-Control-Allow-Origin", "*")
//                        .dedupeResponseHeader("Access-Control-Allow-Credentials Access-Control-Allow-Origin", "RETAIN_FIRST")
                    )
                    .uri(testUrl)
                )
                .build();

        // rewritePath는 path를 아예 바꿔주는 역할. http://localhost:8100/post 호출 시 get method로 호출해도 "405 Method Not Allowed" 안나오고 성공.
        RouteLocator rewriteRouteLocator = builder.routes().route(p -> p
                .path("/**")
                .filters(f -> f
                    .rewritePath("/post", "/get")
                )
                .uri(testUrl)).build();

        // fallback url을 외부 url로 설정
        RouteLocator extFallbackRouteLocator = builder.routes()
                .route("normal", p -> p
                    .path("/delay/3")
                    .filters(f -> f.circuitBreaker(config -> config
                            .setName("mycmd")
                            .setFallbackUri("forward:/fallback2")
                        )
                        .fallbackHeaders(config -> config.setExecutionExceptionTypeHeaderName("Test-Header"))
                        .mapRequestHeader("Blue", "X-Request-Red")
                    )
                    .uri(testUrl)
                )
                .route("fallback", p -> p
                        .path("/fallback2")
                        .uri("https://www.naver.com")
                )
                .build();

        // path 앞에 고정 값을 붙여 주는 것이라고 하는데 잘 테스트가 안됨. /hello를 호출하게 되면 /mypath/hello로 변경해서 route 해 줌.
        RouteLocator prefixRouteLocator = builder.routes()
                .route("prefixpath_route", p -> p
                    .path("/**")
                    .filters(f -> f.prefixPath("/mypath"))
                    .uri(testUrl)
                )
                .build();

        // 300번대 status code를 주고 url로 이동 시킨다. 200번대로 하니 이동 안됨.
        URL url = new URL("https://www.daum.net");
        RouteLocator redirectRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
                    .filters(f -> f.redirect(HttpStatus.FOUND, url))
                    .uri(testUrl)
                )
                .build();

        // 특정 request, response header를 제거하고 전달...
        RouteLocator removeRequestHeaderRouteLocator = builder.routes()
                .route(p -> p
                    .path("/headers")
                    .filters(f -> f
                        .removeRequestHeader("X-Request-Foo")
                        .removeResponseHeader("Server"))
                    .uri(testUrl)
                )
                .build();

        // 파라미터 제거하고 전달. 아래는 foo라는 파라미터를 제거하고 전달
        RouteLocator removeRequestParamrouteLocator = builder.routes()
                .route(p -> p
                    .path("/get")
                    .filters(f -> f
                        .removeRequestParameter("foo")
                    )
                    .uri(testUrl)
                )
                .build();

        // rewrite 된 곳의 response 된 헤더 값을 변경
        RouteLocator rewriteResponseHeaderRouteLocator = builder.routes()
                .route(p -> p
                    .path("/rewrite-response-header")
                    .filters(f -> f
                        .rewritePath("/rewrite-response-header", "/headers")
                        .rewriteResponseHeader("server", "gunicorn/19.9.0", "gunicorn/20.9.0")
                    )
                    .uri(testUrl)
                )
                .build();

        // 매뉴얼 상 WebSession::save 를 하는 거라고 하는데 정확한 의미는 모르겠음. GW 이전에서 넘어오는 세션을 유지시켜 준다는 뜻인가?
        // Spring Security를 쓰게 된다면 중요하다고 함.
        RouteLocator saveSessionRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
//                    .filters(f -> f.saveSession())
                    .filters(GatewayFilterSpec::saveSession)
                    .uri(testUrl)
                )
                .build();

        // 보안과 관련된 헤더 조작을 하는 것으로 보이는데 정확한 사용법은 모르겠다.
        RouteLocator secureHeadersRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
                    .filters(f -> f
                        .secureHeaders(
                            config -> config.withDefaults(secureHeadersProperties)
                        )
                    )
                    .uri(testUrl)
                )
                .build();

        // SetPath 경로를 바꿔 준다. rewrite와 다른게 무엇?
        RouteLocator setPathRouteLocator = builder.routes()
                .route(p -> p
                    .path("/red/{segment}")
                    .filters(f -> f
                        .setPath("/{segment}")
                    )
                    .uri(testUrl)
                )
                .build();

        // setRequestHeader... path 또는 host 정보와 같은 것을 application에서 사용하고자 할 경우 헤더에 셋팅해서 사용.
        RouteLocator setRequestHeaderRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
                    .filters(f -> f
                        .setRequestHeader("X-Request-Red", "Blue")
                        .setResponseHeader("X-Request-Red", "Blue")
                    )
                    .uri(testUrl)
                )
                .build();

        // 응답 http status 코드 값을 변경하여 내려준다.
        RouteLocator setStatusRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
                    .filters(f -> f
                        .setStatus(HttpStatus.UNAUTHORIZED)
                    )
                    .uri(testUrl)
                )
                .build();

        // Retry
        RouteLocator retryRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
                    .filters(f -> f
                        .retry(config -> config
                            .setRetries(3)
                            .setStatuses(HttpStatus.SERVICE_UNAVAILABLE, HttpStatus.NOT_FOUND)
                            .setMethods(HttpMethod.GET, HttpMethod.POST)
                            .setSeries(HttpStatus.Series.CLIENT_ERROR, HttpStatus.Series.SERVER_ERROR)
                            .setBackoff(new RetryGatewayFilterFactory.BackoffConfig(Duration.ofMillis(10), Duration.ofMillis(50), 2, false))
                        )
                    )
                    .uri("https://www.naver.com/get")
                )
                .build();

        // requestSize filter (max size limit)
        RouteLocator requestSizeRouteLocator = builder.routes()
                .route(p -> p
                    .path("/**")
                    .filters(f -> f
                        .setRequestSize(DataSize.ofBytes(10))
                    )
                    .uri(testUrl)
                )
                .build();

        // setRequestHostHeader
        RouteLocator setRequestHostHeaderRouteLocator = builder.routes()
                .route(p -> p
                    .path("/headers")
                    .filters(f -> f
                        .setHostHeader("example.org")
                    )
                    .uri(testUrl)
                )
                .build();

        return headerAddRouteLocator;
    }

자세한 내용은 주석 참고

Between

  • after, before를 같이 쓰기 위해서는 between을 사용할 수 있음.

 

dedupeResponseHeader

  • dedupe 라는 영어 단어를 찾아도 알 수 있는 단어가 나오지 않음.
  • dedupeResponseHeader method의 javadoc을 근거로 보면 dedupeResponseHeader 메소드의 첫 번째 파라미터에는 중복을 없애고 싶은 헤더 이름을, 두 번째 파라미터에는 중복 제거 전략을 명시하는 것으로 보임.
  • 첫 번째 파라미터는 스페이스 구분자를 사용해서 이어서 쓸 수 있음.
  • dedupeResponseHeader 주석을 하고 호출하게 되면 아래와 같이 나옴.
  • 주석을 풀고 호출하면 중복이 제거 되어서 나옴
  • 두 번째 파라미터인 strategy에 쓸 수 있는 값은 RETAIN_FIRST, RETAIN_LAST, RETAIN_UNIQUE 중 하나를 쓸 수 있으며 String을 값을 받음. 내부 적으로는 enum 값을 아래와 같이 셋팅해서 사용 중
org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.java

	public enum Strategy {

		/**
		 * Default: Retain the first value only.
		 */
		RETAIN_FIRST,

		/**
		 * Retain the last value only.
		 */
		RETAIN_LAST,

		/**
		 * Retain all unique values in the order of their first encounter.
		 */
		RETAIN_UNIQUE

	}

dedupeResponseHeader를 사용하게 되면 헤더 중복이 제거 된다.

 

fallbackHeaders

  • fallback을 설정할 때 fallbackHeaders를 설정하게 되는 경우는 정확히 테스트 못 해 봤지만, 매뉴얼 상 Exception 내용을 헤더에 담아 보내주는 것으로 보인다. - docs.spring.io/spring-cloud-gateway/docs/2.2.6.BUILD-SNAPSHOT/reference/html/#fallback-headers 참고
    • 기본 값은 위 매뉴얼에 나와 있는 것 처럼 아래의 내용으로 전달 되는 듯
      • executionExceptionTypeHeaderName ("Execution-Exception-Type")
      • executionExceptionMessageHeaderName ("Execution-Exception-Message")
      • rootCauseExceptionTypeHeaderName ("Root-Cause-Exception-Type")
      • rootCauseExceptionMessageHeaderName ("Root-Cause-Exception-Message")

mapRequestHeader

  • mapRequestHeader는 From으로 받은 헤더를 To로 전달해 주는 것으로 보임.

 

secureHeaders

 

Appcanary - Everything you need to know about HTTP security headers

Some physicists 28 years ago needed a way to easily share experimental data and thus the web was born. This was generally considered to be a good move. Unfortunately, everything physicists touch — from trigonometry to the strong nuclear force — eventua

blog.appcanary.com

  • @Autowired 시킨  "private SecureHeadersProperties secureHeadersProperties" 값을 "secureHeaders(config -> config.withDefaults(secureHeadersProperties))" 해 주게 되면 response header에 보안과 관련된 헤더가 붙어 나오게 된다.
    • withDefaults는 org.springframework.cloud.gateway.filter.factory에 위치해 있다.
  • 아래는 보안과 관련된 헤더를 적용하지 않은 경우 http://httpbin.org 로부터 받게 된 헤더 정보이다.

일반적인 헤더 정보만 반환한다.

  • 반면에 secureHeaders를 기본 Properties로 적용시키게 되면 아래와 같이 나오게 된다.

cross site script 등을 방어할 수 있는 헤더가 추가 되었다.

  • 이와 관련하여 자신의 환경에 맞게 커스터마이징을 하려면 각 헤더별로 set을 하면 된다.
  • 기본값에 Set 한  SecureHeadersPropertiesorg.springframework.cloud.gateway.filter.factory에 위치한 SecureHeadersProperties 클래스이며 "@ConfigurationProperties("spring.cloud.gateway.filter.secure-headers")"로 정의 되어져 있다.
    • 필요한 헤더값이 궁금하면 들어가서 살펴볼 수 있다.

SecureHeadersProperties 클래스

setStatus

  • response의 헤더 값을 변경하여 내려보내줄 수 있다.
  • filters(f -> f.setStatus(HttpStatus.UNAUTHORIZED)) 와 같이 하게 될 경우 401 status 값을 리턴한다.
  • 원래의 http status 값을 헤더에 포함시켜 내려 주고 싶으면 yml 파일에 다음과 같이 적으면 된다.
spring:
  cloud:
    gateway:
      set-status:
        original-status-header-name: original-http-status

위와 같이 설정을 하게 될 경우에는 다음과 같은 모습으로 헤더가 내려온다.

원래 200으로 내려 왔지만 401로 바꾸어 내려주게 된다.

Retry

  • retry 하는 경우를 정의하게 되면 설정 값에 따라 재시도를 하게 된다.
  • setRetries(3) : 3회 재시도 한다
  • setStatuses(HttpStatus.SERVICE_UNAVAILABLE) : http status가 SERVICE_UNAVAILABLE(503)인 경우 재시도 한다.
    • HttpStatus는 org.springframework.http를 사용한다.
  • setMethods(HttpMethod.GET, HttpMethod.POST) : 해당 메소드를 사용하는 경우
  • setSeries(HttpStatus.Series.CLIENT_ERROR, HttpStatus.Series.SERVER_ERROR) : HttpStatus 코드를 일일히 명시하기 힘들경우 1xx, 2xx 식으로 정의 가능. 해당 값은 org.springframework.http.HttpStatus 안에 enum으로 정의 된 Series를 사용
    • INFORMATIONAL(1)
    • SUCCESSFUL(2)
    • REDIRECTION(3)
    • CLIENT_ERROR(4)
    • SERVER_ERROR(5)
  • setBackoff(new RetryGatewayFilterFactory.BackoffConfig(Duration.ofMillis(10), Duration.ofMillis(50), 2, false))
    • 매뉴얼 상 retry를 지수로 감소시키는 것을 설정하는 것으로 보임
    • 파라미터로 넘기는 것은 정적 클래스인 RetryGatewayFilterFactory.BackoffConfig 이며 각 파라미터는 다음과 같음
    • 소스 예제처럼 셋팅할 경우 처음 10밀리 세컨드로 재시도 하고 그 이후 50밀리 세컨드가 될 때까지 지수 형태로 천천히 재시도 하는 것으로 보임. 매뉴얼 참고 : docs.spring.io/spring-cloud-gateway/docs/2.2.6.BUILD-SNAPSHOT/reference/html/#the-retry-gatewayfilter-factory
 

Spring Cloud Gateway

This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them

docs.spring.io

  • 실제 Get, CLIENT_ERROR 으로 설정한 상태에서 없는 URL로 route를 하게 되면 로컬 컴퓨터 기존 250ms 걸리던 호출이 retry로 인해 1.5 ~ 2초 이상 걸리는 것을 볼 수 있음
  • log를 보면 아래와 같이 재시도 하는 모습을 볼 수 있다. 404 not found도 재시도 하도록 해 놔서 아래와 같이 테스트가 가능하다.

초기 10m/s로 해 놓은 값에 따라 재시도를 하는 것을 볼 수 있다.

RequestSize

  • 파일 업로드 시 파일 사이즈의 제한을 줄 수 있다.
  • Postman으로 다음과 같이 테스트 해 보면 HttpStatus code 413이 내려오고 헤더 정보에 에러 메시지(Request size is larger than permissible limit. Request size is 701.3 kB where permissible limit is 10 B)가 아래와 같이 내려 온다.

Postman으로 file upload 테스트시 413 코드 리턴

  • 선언하지 않을 경우 기본 용량은 5MB 인 것으로 보임 (The default request size is set to five MB if not provided as a filter argument in the route definition.)

 

내용이 길어져서 Global Filter 부분 부터는 나눠서 기록한다.

 

 

반응형