[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…
Reference in New Issue