[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:
psevestre 2022-06-26 19:06:04 -03:00 committed by GitHub
parent ff3cc7b948
commit 676535b04d
5 changed files with 365 additions and 0 deletions

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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,***

View File

@ -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());
}
}

View File

@ -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;
}
}
}