Skip to content

Commit 2ff5e62

Browse files
authored
[BAEL-5258] Processing the Response Body in Spring Cloud Gateway (eugenp#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
1 parent f842ec9 commit 2ff5e62

File tree

5 files changed

+365
-0
lines changed

5 files changed

+365
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories;
2+
3+
import java.util.Arrays;
4+
import java.util.List;
5+
import java.util.regex.Pattern;
6+
7+
import org.reactivestreams.Publisher;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.springframework.cloud.gateway.filter.GatewayFilter;
11+
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
12+
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
13+
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
14+
import org.springframework.stereotype.Component;
15+
import org.springframework.web.server.ServerWebExchange;
16+
17+
import com.fasterxml.jackson.core.TreeNode;
18+
import com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.node.ArrayNode;
20+
import com.fasterxml.jackson.databind.node.ObjectNode;
21+
import com.fasterxml.jackson.databind.node.TextNode;
22+
23+
import reactor.core.publisher.Mono;
24+
25+
@Component
26+
public class ScrubResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<ScrubResponseGatewayFilterFactory.Config> {
27+
28+
final Logger logger = LoggerFactory.getLogger(ScrubResponseGatewayFilterFactory.class);
29+
private ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory;
30+
31+
public ScrubResponseGatewayFilterFactory(ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilterFactory) {
32+
super(Config.class);
33+
this.modifyResponseBodyFilterFactory = modifyResponseBodyFilterFactory;
34+
}
35+
36+
@Override
37+
public List<String> shortcutFieldOrder() {
38+
return Arrays.asList("fields", "replacement");
39+
}
40+
41+
42+
@Override
43+
public GatewayFilter apply(Config config) {
44+
45+
return modifyResponseBodyFilterFactory
46+
.apply(c -> c.setRewriteFunction(JsonNode.class, JsonNode.class, new Scrubber(config)));
47+
}
48+
49+
public static class Config {
50+
51+
private String fields;
52+
private String replacement;
53+
54+
55+
public String getFields() {
56+
return fields;
57+
}
58+
public void setFields(String fields) {
59+
this.fields = fields;
60+
}
61+
public String getReplacement() {
62+
return replacement;
63+
}
64+
public void setReplacement(String replacement) {
65+
this.replacement = replacement;
66+
}
67+
}
68+
69+
70+
public static class Scrubber implements RewriteFunction<JsonNode,JsonNode> {
71+
private final Pattern fields;
72+
private final String replacement;
73+
74+
public Scrubber(Config config) {
75+
this.fields = Pattern.compile(config.getFields());
76+
this.replacement = config.getReplacement();
77+
}
78+
79+
@Override
80+
public Publisher<JsonNode> apply(ServerWebExchange t, JsonNode u) {
81+
return Mono.just(scrubRecursively(u));
82+
}
83+
84+
private JsonNode scrubRecursively(JsonNode u) {
85+
if ( !u.isContainerNode()) {
86+
return u;
87+
}
88+
89+
if ( u.isObject()) {
90+
ObjectNode node = (ObjectNode)u;
91+
node.fields().forEachRemaining((f) -> {
92+
if ( fields.matcher(f.getKey()).matches() && f.getValue().isTextual()) {
93+
f.setValue(TextNode.valueOf(replacement));
94+
}
95+
else {
96+
f.setValue(scrubRecursively(f.getValue()));
97+
}
98+
});
99+
}
100+
else if ( u.isArray()) {
101+
ArrayNode array = (ArrayNode)u;
102+
for ( int i = 0 ; i < array.size() ; i++ ) {
103+
array.set(i, scrubRecursively(array.get(i)));
104+
}
105+
}
106+
107+
return u;
108+
}
109+
}
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.global;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
@ConfigurationProperties("logging.global")
6+
public class LoggingGlobalFilterProperties {
7+
8+
private boolean enabled;
9+
private boolean requestHeaders;
10+
private boolean requestBody;
11+
private boolean responseHeaders;
12+
private boolean responseBody;
13+
14+
public boolean isEnabled() {
15+
return enabled;
16+
}
17+
public void setEnabled(boolean enabled) {
18+
this.enabled = enabled;
19+
}
20+
public boolean isRequestHeaders() {
21+
return requestHeaders;
22+
}
23+
public void setRequestHeaders(boolean requestHeaders) {
24+
this.requestHeaders = requestHeaders;
25+
}
26+
public boolean isRequestBody() {
27+
return requestBody;
28+
}
29+
public void setRequestBody(boolean requestBody) {
30+
this.requestBody = requestBody;
31+
}
32+
public boolean isResponseHeaders() {
33+
return responseHeaders;
34+
}
35+
public void setResponseHeaders(boolean responseHeaders) {
36+
this.responseHeaders = responseHeaders;
37+
}
38+
public boolean isResponseBody() {
39+
return responseBody;
40+
}
41+
public void setResponseBody(boolean responseBody) {
42+
this.responseBody = responseBody;
43+
}
44+
45+
46+
47+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
spring:
2+
cloud:
3+
gateway:
4+
routes:
5+
- id: rewrite_with_scrub
6+
uri: ${rewrite.backend.uri:http://example.com}
7+
predicates:
8+
- Path=/v1/customer/**
9+
filters:
10+
- RewritePath=/v1/customer/(?<segment>.*),/api/$\{segment}
11+
- ScrubResponse=ssn,***
12+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories.ScrubResponseGatewayFilterFactory.Config;
8+
import com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories.ScrubResponseGatewayFilterFactory.Scrubber;
9+
import com.fasterxml.jackson.core.JsonFactory;
10+
import com.fasterxml.jackson.core.JsonParser;
11+
import com.fasterxml.jackson.core.ObjectCodec;
12+
import com.fasterxml.jackson.core.TreeNode;
13+
import com.fasterxml.jackson.databind.JsonNode;
14+
import com.fasterxml.jackson.databind.ObjectMapper;
15+
16+
import reactor.core.publisher.Mono;
17+
18+
class ScrubResponseGatewayFilterFactoryUnitTest {
19+
20+
private static final String JSON_WITH_FIELDS_TO_SCRUB = "{\r\n"
21+
+ " \"name\" : \"John Doe\",\r\n"
22+
+ " \"ssn\" : \"123-45-9999\",\r\n"
23+
+ " \"account\" : \"9999888877770000\"\r\n"
24+
+ "}";
25+
26+
27+
@Test
28+
void givenJsonWithFieldsToScrub_whenApply_thenScrubFields() throws Exception{
29+
30+
JsonFactory jf = new JsonFactory(new ObjectMapper());
31+
JsonParser parser = jf.createParser(JSON_WITH_FIELDS_TO_SCRUB);
32+
JsonNode root = parser.readValueAsTree();
33+
34+
Config config = new Config();
35+
config.setFields("ssn|account");
36+
config.setReplacement("*");
37+
Scrubber scrubber = new ScrubResponseGatewayFilterFactory.Scrubber(config);
38+
39+
JsonNode scrubbed = Mono.from(scrubber.apply(null, root)).block();
40+
assertNotNull(scrubbed);
41+
assertEquals("*", scrubbed.get("ssn").asText());
42+
}
43+
44+
@Test
45+
void givenJsonWithoutFieldsToScrub_whenApply_theBodUnchanged() throws Exception{
46+
47+
JsonFactory jf = new JsonFactory(new ObjectMapper());
48+
JsonParser parser = jf.createParser(JSON_WITH_FIELDS_TO_SCRUB);
49+
JsonNode root = parser.readValueAsTree();
50+
51+
Config config = new Config();
52+
config.setFields("xxxx");
53+
config.setReplacement("*");
54+
Scrubber scrubber = new ScrubResponseGatewayFilterFactory.Scrubber(config);
55+
56+
JsonNode scrubbed = Mono.from(scrubber.apply(null, root)).block();
57+
assertNotNull(scrubbed);
58+
assertNotEquals("*", scrubbed.get("ssn").asText());
59+
}
60+
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.baeldung.springcloudgateway.customfilters.gatewayapp.filters.factories;
2+
3+
import java.io.IOException;
4+
import java.net.InetSocketAddress;
5+
import java.util.Collections;
6+
7+
import org.junit.AfterClass;
8+
import org.junit.BeforeClass;
9+
import org.junit.jupiter.api.Test;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
import org.springframework.boot.CommandLineRunner;
14+
import org.springframework.boot.test.context.SpringBootTest;
15+
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
16+
import org.springframework.boot.test.context.TestConfiguration;
17+
import org.springframework.boot.web.server.LocalServerPort;
18+
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory;
19+
import org.springframework.cloud.gateway.route.RouteLocator;
20+
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
21+
import org.springframework.context.annotation.Bean;
22+
import org.springframework.context.annotation.Primary;
23+
import org.springframework.http.MediaType;
24+
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
25+
import org.springframework.security.config.web.server.ServerHttpSecurity;
26+
import org.springframework.security.web.server.SecurityWebFilterChain;
27+
import org.springframework.test.web.reactive.server.WebTestClient;
28+
29+
import com.sun.net.httpserver.HttpServer;
30+
31+
import reactor.netty.http.client.HttpClient;
32+
33+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
34+
public class ScrubResponseGatewayFilterLiveTest {
35+
36+
private static Logger log = LoggerFactory.getLogger(ScrubResponseGatewayFilterLiveTest.class);
37+
38+
private static final String JSON_WITH_FIELDS_TO_SCRUB = "{\r\n"
39+
+ " \"name\" : \"John Doe\",\r\n"
40+
+ " \"ssn\" : \"123-45-9999\",\r\n"
41+
+ " \"account\" : \"9999888877770000\"\r\n"
42+
+ "}";
43+
44+
private static final String JSON_WITH_SCRUBBED_FIELDS = "{\r\n"
45+
+ " \"name\" : \"John Doe\",\r\n"
46+
+ " \"ssn\" : \"*\",\r\n"
47+
+ " \"account\" : \"9999888877770000\"\r\n"
48+
+ "}";
49+
50+
@LocalServerPort
51+
String port;
52+
53+
@Autowired
54+
private WebTestClient client;
55+
56+
@Autowired HttpServer server;
57+
58+
@Test
59+
public void givenRequestToScrubRoute_thenResponseScrubbed() {
60+
61+
client.get()
62+
.uri("/scrub")
63+
.accept(MediaType.APPLICATION_JSON)
64+
.exchange()
65+
.expectStatus()
66+
.is2xxSuccessful()
67+
.expectHeader()
68+
.contentType(MediaType.APPLICATION_JSON)
69+
.expectBody()
70+
.json(JSON_WITH_SCRUBBED_FIELDS);
71+
}
72+
73+
74+
@TestConfiguration
75+
public static class TestRoutesConfiguration {
76+
77+
78+
@Bean
79+
public RouteLocator scrubSsnRoute(RouteLocatorBuilder builder, ScrubResponseGatewayFilterFactory scrubFilterFactory, SetPathGatewayFilterFactory pathFilterFactory, HttpServer server ) {
80+
81+
log.info("[I92] Creating scrubSsnRoute...");
82+
83+
int mockServerPort = server.getAddress().getPort();
84+
ScrubResponseGatewayFilterFactory.Config config = new ScrubResponseGatewayFilterFactory.Config();
85+
config.setFields("ssn");
86+
config.setReplacement("*");
87+
88+
SetPathGatewayFilterFactory.Config pathConfig = new SetPathGatewayFilterFactory.Config();
89+
pathConfig.setTemplate("/customer");
90+
91+
return builder.routes()
92+
.route("scrub_ssn",
93+
r -> r.path("/scrub")
94+
.filters(
95+
f -> f
96+
.filter(scrubFilterFactory.apply(config))
97+
.filter(pathFilterFactory.apply(pathConfig)))
98+
.uri("http://localhost:" + mockServerPort ))
99+
.build();
100+
}
101+
102+
@Bean
103+
public SecurityWebFilterChain testFilterChain(ServerHttpSecurity http ) {
104+
105+
// @formatter:off
106+
return http.authorizeExchange()
107+
.anyExchange()
108+
.permitAll()
109+
.and()
110+
.build();
111+
// @formatter:on
112+
}
113+
114+
@Bean
115+
public HttpServer mockServer() throws IOException {
116+
117+
log.info("[I48] Starting mock server...");
118+
119+
HttpServer server = HttpServer.create(new InetSocketAddress(0),0);
120+
server.createContext("/customer", (exchange) -> {
121+
exchange.getResponseHeaders().set("Content-Type", "application/json");
122+
123+
byte[] response = JSON_WITH_FIELDS_TO_SCRUB.getBytes("UTF-8");
124+
exchange.sendResponseHeaders(200,response.length);
125+
exchange.getResponseBody().write(response);
126+
});
127+
128+
server.setExecutor(null);
129+
server.start();
130+
131+
log.info("[I65] Mock server started. port={}", server.getAddress().getPort());
132+
return server;
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)