[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