From ed6d85a620f526232de789b25ef044a79c367058 Mon Sep 17 00:00:00 2001 From: Forb Yuan Date: Fri, 19 Apr 2024 01:23:33 +0800 Subject: [PATCH] [BAEL-6276] Exploring the New Filters on Spring Cloud Gateway (#16341) * BAEL-6276: Fix the broken tests * BAEL-6276: Add request header AddRequestHeadersIfNotPresent * BAEL-6276: Add response header RemoveJsonAttributesResponseBody * BAEL-6276: Example for CacheRequestBody * BAEL-6276: Example for LocalResponseCache * BAEL-6276: Add `redis-rate-limiter.requestedTokens` for `RequestRateLimiter` --- .../spring-cloud-gateway/pom.xml | 11 +++- .../webfilters/CacheEvaluationFilter.java | 22 ++++++++ .../main/resources/application-webfilters.yml | 51 ++++++++++++++++--- .../WebFilterFactoriesLiveTest.java | 36 +++++++++++-- 4 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-modules/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/CacheEvaluationFilter.java diff --git a/spring-cloud-modules/spring-cloud-gateway/pom.xml b/spring-cloud-modules/spring-cloud-gateway/pom.xml index 5d0594a259..18c306e61c 100644 --- a/spring-cloud-modules/spring-cloud-gateway/pom.xml +++ b/spring-cloud-modules/spring-cloud-gateway/pom.xml @@ -65,6 +65,14 @@ jakarta.validation-api ${jakarta.validation-api.version} + + org.springframework.boot + spring-boot-starter-cache + + + com.github.ben-manes.caffeine + caffeine + org.springframework.boot spring-boot-starter-actuator @@ -179,11 +187,12 @@ + 3.2.3 2023.0.0 8.0.1.Final 0.7.2 9.19 - + 5.10.2 3.0.2 diff --git a/spring-cloud-modules/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/CacheEvaluationFilter.java b/spring-cloud-modules/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/CacheEvaluationFilter.java new file mode 100644 index 0000000000..bf23485402 --- /dev/null +++ b/spring-cloud-modules/spring-cloud-gateway/src/main/java/com/baeldung/springcloudgateway/webfilters/CacheEvaluationFilter.java @@ -0,0 +1,22 @@ +package com.baeldung.springcloudgateway.webfilters; + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; + +import reactor.core.publisher.Mono; + +@Component +public class CacheEvaluationFilter implements GlobalFilter { + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + String body = exchange.getAttribute(ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR); + if (body != null) { + exchange.getResponse().getHeaders().add("My-Header-Cache", body); + } + return chain.filter(exchange); + } +} diff --git a/spring-cloud-modules/spring-cloud-gateway/src/main/resources/application-webfilters.yml b/spring-cloud-modules/spring-cloud-gateway/src/main/resources/application-webfilters.yml index 3348cbbba0..79aa8f3a04 100644 --- a/spring-cloud-modules/spring-cloud-gateway/src/main/resources/application-webfilters.yml +++ b/spring-cloud-modules/spring-cloud-gateway/src/main/resources/application-webfilters.yml @@ -9,6 +9,11 @@ spring: port: 6379 cloud: gateway: + filter: + local-response-cache: + enabled: true + timeToLive: 20m + size: 6MB routes: - id: request_header_route uri: https://httpbin.org @@ -17,6 +22,7 @@ spring: filters: - AddRequestHeader=My-Header-Good,Good - AddRequestHeader=My-Header-Remove,Remove + - AddRequestHeadersIfNotPresent=My-Header-Absent:Absent - AddRequestParameter=var, good - AddRequestParameter=var2, remove - MapRequestHeader=My-Header-Good, My-Header-Bad @@ -31,17 +37,18 @@ spring: predicates: - Path=/header/post/** filters: - - AddResponseHeader=My-Header-Good,Good - - AddResponseHeader=My-Header-Set,Good - - AddResponseHeader=My-Header-Rewrite, password=12345678 - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin - - AddResponseHeader=My-Header-Remove,Remove - SetResponseHeader=My-Header-Set, Set - RemoveResponseHeader=My-Header-Remove - RewriteResponseHeader=My-Header-Rewrite, password=[^&]+, password=*** - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , + - RemoveJsonAttributesResponseBody=form,Accept,true + - AddResponseHeader=My-Header-Good,Good + - AddResponseHeader=My-Header-Set,Good + - AddResponseHeader=My-Header-Remove,Remove + - AddResponseHeader=My-Header-Rewrite,password=12345678 - StripPrefix=1 - + - id: path_route uri: https://httpbin.org predicates: @@ -88,7 +95,18 @@ spring: maxBackoff: 50ms factor: 2 basedOnPreviousValue: false - + + - id: circuitbreaker_route + uri: https://httpbin.org + predicates: + - Path=/status/504 + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/anything + - RewritePath=/status/504, /anything + - id: request_rate_limiter uri: https://httpbin.org predicates: @@ -99,4 +117,23 @@ spring: args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 5 - key-resolver: "#{@userKeyResolver}" \ No newline at end of file + redis-rate-limiter.requestedTokens: 1 + key-resolver: "#{@userKeyResolver}" + + - id: cache_request_body_route + uri: https://httpbin.org + predicates: + - Path=/cache/post/** + filters: + - StripPrefix=1 + - name: CacheRequestBody + args: + bodyClass: java.lang.String + + - id: cache_response_body_route + uri: https://httpbin.org + predicates: + - Path=/cache/get/** + filters: + - StripPrefix=1 + - LocalResponseCache=10s,20MB diff --git a/spring-cloud-modules/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java b/spring-cloud-modules/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java index 0caf92c2e8..1fc28b5251 100644 --- a/spring-cloud-modules/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java +++ b/spring-cloud-modules/spring-cloud-gateway/src/test/java/com/baeldung/springcloudgateway/webfilters/WebFilterFactoriesLiveTest.java @@ -1,11 +1,8 @@ package com.baeldung.springcloudgateway.webfilters; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import org.assertj.core.api.Condition; import org.json.JSONException; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; @@ -23,9 +20,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +import org.springframework.web.reactive.function.BodyInserters; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@ActiveProfiles("webfilters") +@ActiveProfiles({"webfilters","nosecurity"}) public class WebFilterFactoriesLiveTest { @LocalServerPort @@ -55,6 +53,7 @@ public class WebFilterFactoriesLiveTest { assertThat(headers.getString("My-Header-Good")).isEqualTo("Good"); assertThat(headers.getString("My-Header-Bad")).isEqualTo("Good"); assertThat(headers.getString("My-Header-Set")).isEqualTo("Set"); + assertThat(headers.getString("My-Header-Absent")).isEqualTo("Absent"); assertTrue(headers.isNull("My-Header-Remove")); JSONObject vars = json.getJSONObject("args"); assertThat(vars.getString("var")).isEqualTo("good"); @@ -75,7 +74,10 @@ public class WebFilterFactoriesLiveTest { .expectHeader() .valueEquals("My-Header-Good", "Good") .expectHeader() - .doesNotExist("My-Header-Remove"); + .doesNotExist("My-Header-Remove") + .expectBody() + .jsonPath("$.headers.Accept").doesNotExist() + .jsonPath("form").doesNotExist(); } @Test @@ -133,4 +135,28 @@ public class WebFilterFactoriesLiveTest { JSONObject json = new JSONObject(response.getBody()); assertThat(json.getString("url")).contains("anything"); } + + @Test + public void whenCallCachePostThroughGateway_thenMyHeaderCacheIsSet() { + ResponseSpec response = client.post() + .uri("/cache/post") + .body(BodyInserters.fromValue("CachedBody")) + .exchange(); + + response.expectStatus() + .isOk() + .expectHeader() + .valueEquals("My-Header-Cache", "CachedBody"); + } + + @Test + public void whenCallCacheGetThroughGateway_thenCacheControlIsSet() { + ResponseSpec response = client.get() + .uri("/cache/get") + .exchange(); + + response.expectStatus().isOk() + .expectHeader() + .valueEquals("Cache-Control", "max-age=10"); + } }