[BAEL-5258] Processing the Response Body in Spring Cloud Gateway (#12414)
* [BAEL-4849] Article code * [BAEL-4968] Article code * [BAEL-4968] Article code * [BAEL-4968] Article code * [BAEL-4968] Remove extra comments * [BAEL-5258] Article Code
This commit is contained in:
		
							parent
							
								
									ff3cc7b948
								
							
						
					
					
						commit
						676535b04d
					
				| @ -0,0 +1,110 @@ | ||||
| package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.regex.Pattern; | ||||
| 
 | ||||
| import org.reactivestreams.Publisher; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.cloud.gateway.filter.GatewayFilter; | ||||
| import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; | ||||
| import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory; | ||||
| import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.web.server.ServerWebExchange; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.TreeNode; | ||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||
| import com.fasterxml.jackson.databind.node.ArrayNode; | ||||
| import com.fasterxml.jackson.databind.node.ObjectNode; | ||||
| import com.fasterxml.jackson.databind.node.TextNode; | ||||
| 
 | ||||
| import reactor.core.publisher.Mono; | ||||
| 
 | ||||
| @Component | ||||
| public class ScrubResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<ScrubResponseGatewayFilterFactory.Config> { | ||||
| 
 | ||||
|     final Logger logger = LoggerFactory.getLogger(ScrubResponseGatewayFilterFactory.class); | ||||
|     private ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory; | ||||
| 
 | ||||
|     public ScrubResponseGatewayFilterFactory(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) { | ||||
|         super(Config.class); | ||||
|         this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory; | ||||
|     } | ||||
|      | ||||
|     @Override | ||||
|     public List<String> shortcutFieldOrder() { | ||||
|         return Arrays.asList("fields", "replacement"); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @Override | ||||
|     public GatewayFilter apply(Config config) { | ||||
|          | ||||
|         return modifyResponseBodyFilterFactory | ||||
|           .apply(c -> c.setRewriteFunction(JsonNode.class, JsonNode.class, new Scrubber(config))); | ||||
|     } | ||||
| 
 | ||||
|     public static class Config { | ||||
|          | ||||
|         private String fields; | ||||
|         private String replacement; | ||||
|          | ||||
|          | ||||
|         public String getFields() { | ||||
|             return fields; | ||||
|         } | ||||
|         public void setFields(String fields) { | ||||
|             this.fields = fields; | ||||
|         } | ||||
|         public String getReplacement() { | ||||
|             return replacement; | ||||
|         } | ||||
|         public void setReplacement(String replacement) { | ||||
|             this.replacement = replacement; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|      | ||||
|     public static class Scrubber implements RewriteFunction<JsonNode,JsonNode> { | ||||
|         private final Pattern fields; | ||||
|         private final String replacement; | ||||
|          | ||||
|         public Scrubber(Config config) { | ||||
|             this.fields = Pattern.compile(config.getFields()); | ||||
|             this.replacement = config.getReplacement(); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public Publisher<JsonNode> apply(ServerWebExchange t, JsonNode u) { | ||||
|             return Mono.just(scrubRecursively(u)); | ||||
|         } | ||||
| 
 | ||||
|         private JsonNode scrubRecursively(JsonNode u) { | ||||
|             if ( !u.isContainerNode()) { | ||||
|                 return u; | ||||
|             } | ||||
|              | ||||
|             if ( u.isObject()) { | ||||
|                 ObjectNode node = (ObjectNode)u; | ||||
|                 node.fields().forEachRemaining((f) -> { | ||||
|                     if ( fields.matcher(f.getKey()).matches() && f.getValue().isTextual()) { | ||||
|                         f.setValue(TextNode.valueOf(replacement)); | ||||
|                     } | ||||
|                     else { | ||||
|                         f.setValue(scrubRecursively(f.getValue())); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|             else if ( u.isArray()) { | ||||
|                 ArrayNode array = (ArrayNode)u; | ||||
|                 for ( int i = 0 ; i < array.size() ; i++ ) { | ||||
|                     array.set(i, scrubRecursively(array.get(i))); | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             return u; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,47 @@ | ||||
| package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.global; | ||||
| 
 | ||||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||||
| 
 | ||||
| @ConfigurationProperties("logging.global") | ||||
| public class LoggingGlobalFilterProperties { | ||||
|      | ||||
|     private boolean enabled; | ||||
|     private boolean requestHeaders; | ||||
|     private boolean requestBody; | ||||
|     private boolean responseHeaders; | ||||
|     private boolean responseBody; | ||||
|      | ||||
|     public boolean isEnabled() { | ||||
|         return enabled; | ||||
|     } | ||||
|     public void setEnabled(boolean enabled) { | ||||
|         this.enabled = enabled; | ||||
|     } | ||||
|     public boolean isRequestHeaders() { | ||||
|         return requestHeaders; | ||||
|     } | ||||
|     public void setRequestHeaders(boolean requestHeaders) { | ||||
|         this.requestHeaders = requestHeaders; | ||||
|     } | ||||
|     public boolean isRequestBody() { | ||||
|         return requestBody; | ||||
|     } | ||||
|     public void setRequestBody(boolean requestBody) { | ||||
|         this.requestBody = requestBody; | ||||
|     } | ||||
|     public boolean isResponseHeaders() { | ||||
|         return responseHeaders; | ||||
|     } | ||||
|     public void setResponseHeaders(boolean responseHeaders) { | ||||
|         this.responseHeaders = responseHeaders; | ||||
|     } | ||||
|     public boolean isResponseBody() { | ||||
|         return responseBody; | ||||
|     } | ||||
|     public void setResponseBody(boolean responseBody) { | ||||
|         this.responseBody = responseBody; | ||||
|     } | ||||
|      | ||||
|      | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| spring: | ||||
|   cloud: | ||||
|     gateway: | ||||
|       routes: | ||||
|       - id: rewrite_with_scrub | ||||
|         uri: ${rewrite.backend.uri:http://example.com} | ||||
|         predicates: | ||||
|         - Path=/v1/customer/** | ||||
|         filters: | ||||
|         - RewritePath=/v1/customer/(?<segment>.*),/api/$\{segment} | ||||
|         - ScrubResponse=ssn,*** | ||||
|          | ||||
| @ -0,0 +1,61 @@ | ||||
| package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; | ||||
| 
 | ||||
| import static org.junit.jupiter.api.Assertions.*; | ||||
| 
 | ||||
| import org.junit.jupiter.api.Test; | ||||
| 
 | ||||
| import com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories.ScrubResponseGatewayFilterFactory.Config; | ||||
| import com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories.ScrubResponseGatewayFilterFactory.Scrubber; | ||||
| import com.fasterxml.jackson.core.JsonFactory; | ||||
| import com.fasterxml.jackson.core.JsonParser; | ||||
| import com.fasterxml.jackson.core.ObjectCodec; | ||||
| import com.fasterxml.jackson.core.TreeNode; | ||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| 
 | ||||
| import reactor.core.publisher.Mono; | ||||
| 
 | ||||
| class ScrubResponseGatewayFilterFactoryUnitTest { | ||||
|      | ||||
|     private static final String JSON_WITH_FIELDS_TO_SCRUB = "{\r\n" | ||||
|         + "  \"name\" : \"John Doe\",\r\n" | ||||
|         + "        \"ssn\"  : \"123-45-9999\",\r\n" | ||||
|         + "        \"account\" : \"9999888877770000\"\r\n" | ||||
|         + "}"; | ||||
| 
 | ||||
|      | ||||
|     @Test | ||||
|     void givenJsonWithFieldsToScrub_whenApply_thenScrubFields() throws Exception{ | ||||
|          | ||||
|         JsonFactory jf = new JsonFactory(new ObjectMapper()); | ||||
|         JsonParser parser = jf.createParser(JSON_WITH_FIELDS_TO_SCRUB); | ||||
|         JsonNode root = parser.readValueAsTree(); | ||||
|          | ||||
|         Config config = new Config(); | ||||
|         config.setFields("ssn|account"); | ||||
|         config.setReplacement("*"); | ||||
|         Scrubber scrubber = new ScrubResponseGatewayFilterFactory.Scrubber(config); | ||||
|          | ||||
|         JsonNode scrubbed = Mono.from(scrubber.apply(null, root)).block(); | ||||
|         assertNotNull(scrubbed); | ||||
|         assertEquals("*", scrubbed.get("ssn").asText()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void givenJsonWithoutFieldsToScrub_whenApply_theBodUnchanged() throws Exception{ | ||||
|          | ||||
|         JsonFactory jf = new JsonFactory(new ObjectMapper()); | ||||
|         JsonParser parser = jf.createParser(JSON_WITH_FIELDS_TO_SCRUB); | ||||
|         JsonNode root = parser.readValueAsTree(); | ||||
|          | ||||
|         Config config = new Config(); | ||||
|         config.setFields("xxxx"); | ||||
|         config.setReplacement("*"); | ||||
|         Scrubber scrubber = new ScrubResponseGatewayFilterFactory.Scrubber(config); | ||||
|          | ||||
|         JsonNode scrubbed = Mono.from(scrubber.apply(null, root)).block(); | ||||
|         assertNotNull(scrubbed); | ||||
|         assertNotEquals("*", scrubbed.get("ssn").asText()); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -0,0 +1,135 @@ | ||||
| package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.net.InetSocketAddress; | ||||
| import java.util.Collections; | ||||
| 
 | ||||
| import org.junit.AfterClass; | ||||
| import org.junit.BeforeClass; | ||||
| import org.junit.jupiter.api.Test; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.boot.CommandLineRunner; | ||||
| import org.springframework.boot.test.context.SpringBootTest; | ||||
| import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; | ||||
| import org.springframework.boot.test.context.TestConfiguration; | ||||
| import org.springframework.boot.web.server.LocalServerPort; | ||||
| import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory; | ||||
| import org.springframework.cloud.gateway.route.RouteLocator; | ||||
| import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.context.annotation.Primary; | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.http.client.reactive.ReactorClientHttpConnector; | ||||
| import org.springframework.security.config.web.server.ServerHttpSecurity; | ||||
| import org.springframework.security.web.server.SecurityWebFilterChain; | ||||
| import org.springframework.test.web.reactive.server.WebTestClient; | ||||
| 
 | ||||
| import com.sun.net.httpserver.HttpServer; | ||||
| 
 | ||||
| import reactor.netty.http.client.HttpClient; | ||||
| 
 | ||||
| @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) | ||||
| public class ScrubResponseGatewayFilterLiveTest { | ||||
|      | ||||
|     private static Logger log = LoggerFactory.getLogger(ScrubResponseGatewayFilterLiveTest.class); | ||||
|      | ||||
|     private static final String JSON_WITH_FIELDS_TO_SCRUB = "{\r\n" | ||||
|         + "  \"name\" : \"John Doe\",\r\n" | ||||
|         + "        \"ssn\"  : \"123-45-9999\",\r\n" | ||||
|         + "        \"account\" : \"9999888877770000\"\r\n" | ||||
|         + "}"; | ||||
| 
 | ||||
|     private static final String JSON_WITH_SCRUBBED_FIELDS = "{\r\n" | ||||
|         + "  \"name\" : \"John Doe\",\r\n" | ||||
|         + "        \"ssn\"  : \"*\",\r\n" | ||||
|         + "        \"account\" : \"9999888877770000\"\r\n" | ||||
|         + "}"; | ||||
|      | ||||
|     @LocalServerPort | ||||
|     String port; | ||||
|      | ||||
|     @Autowired | ||||
|     private WebTestClient client; | ||||
|      | ||||
|     @Autowired HttpServer server; | ||||
|      | ||||
|     @Test | ||||
|     public void givenRequestToScrubRoute_thenResponseScrubbed() { | ||||
|                  | ||||
|         client.get() | ||||
|           .uri("/scrub") | ||||
|           .accept(MediaType.APPLICATION_JSON) | ||||
|           .exchange() | ||||
|           .expectStatus() | ||||
|             .is2xxSuccessful() | ||||
|           .expectHeader() | ||||
|             .contentType(MediaType.APPLICATION_JSON) | ||||
|           .expectBody() | ||||
|             .json(JSON_WITH_SCRUBBED_FIELDS); | ||||
|     } | ||||
|      | ||||
|      | ||||
|     @TestConfiguration | ||||
|     public static class TestRoutesConfiguration { | ||||
|          | ||||
|          | ||||
|         @Bean | ||||
|         public RouteLocator scrubSsnRoute(RouteLocatorBuilder builder, ScrubResponseGatewayFilterFactory scrubFilterFactory, SetPathGatewayFilterFactory pathFilterFactory, HttpServer server ) { | ||||
|              | ||||
|             log.info("[I92] Creating scrubSsnRoute..."); | ||||
|              | ||||
|             int mockServerPort = server.getAddress().getPort(); | ||||
|             ScrubResponseGatewayFilterFactory.Config config = new ScrubResponseGatewayFilterFactory.Config(); | ||||
|             config.setFields("ssn"); | ||||
|             config.setReplacement("*"); | ||||
|              | ||||
|             SetPathGatewayFilterFactory.Config pathConfig = new SetPathGatewayFilterFactory.Config(); | ||||
|             pathConfig.setTemplate("/customer"); | ||||
|              | ||||
|             return builder.routes() | ||||
|               .route("scrub_ssn", | ||||
|                  r -> r.path("/scrub") | ||||
|                    .filters(  | ||||
|                       f -> f | ||||
|                         .filter(scrubFilterFactory.apply(config)) | ||||
|                         .filter(pathFilterFactory.apply(pathConfig))) | ||||
|                    .uri("http://localhost:" + mockServerPort )) | ||||
|               .build(); | ||||
|         } | ||||
|          | ||||
|         @Bean | ||||
|         public SecurityWebFilterChain testFilterChain(ServerHttpSecurity http ) { | ||||
|              | ||||
|             // @formatter:off | ||||
|             return http.authorizeExchange() | ||||
|               .anyExchange() | ||||
|                 .permitAll() | ||||
|               .and() | ||||
|               .build(); | ||||
|             // @formatter:on | ||||
|         } | ||||
|          | ||||
|         @Bean | ||||
|         public HttpServer mockServer() throws IOException { | ||||
|              | ||||
|             log.info("[I48] Starting mock server..."); | ||||
|              | ||||
|             HttpServer server = HttpServer.create(new InetSocketAddress(0),0); | ||||
|             server.createContext("/customer", (exchange) ->  { | ||||
|                 exchange.getResponseHeaders().set("Content-Type", "application/json"); | ||||
|                  | ||||
|                 byte[] response = JSON_WITH_FIELDS_TO_SCRUB.getBytes("UTF-8"); | ||||
|                 exchange.sendResponseHeaders(200,response.length); | ||||
|                 exchange.getResponseBody().write(response); | ||||
|             }); | ||||
|              | ||||
|             server.setExecutor(null); | ||||
|             server.start(); | ||||
|              | ||||
|             log.info("[I65] Mock server started. port={}", server.getAddress().getPort()); | ||||
|             return server; | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user