Skip to content

Commit 41b212d

Browse files
author
Fredrich Ombico
authored
Add RewriteRequestParameter GatewayFilter factory (#3081)
The `RewriteRequestParameter` `GatewayFilter` factory takes a `name` parameter and a `replacement` parameter. It will rewrite the value of the request parameter of the given `name`.
1 parent a76bf7c commit 41b212d

File tree

10 files changed

+311
-0
lines changed

10 files changed

+311
-0
lines changed

docs/src/main/asciidoc/_configprops.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
|spring.cloud.gateway.filter.rewrite-location-response-header.enabled | `+++true+++` | Enables the rewrite-location-response-header filter.
4444
|spring.cloud.gateway.filter.rewrite-location.enabled | `+++true+++` | Enables the rewrite-location filter.
4545
|spring.cloud.gateway.filter.rewrite-path.enabled | `+++true+++` | Enables the rewrite-path filter.
46+
|spring.cloud.gateway.filter.rewrite-request-parameter.enabled | `+++true+++` | Enables the rewrite-request-parameter filter.
4647
|spring.cloud.gateway.filter.rewrite-response-header.enabled | `+++true+++` | Enables the rewrite-response-header filter.
4748
|spring.cloud.gateway.filter.save-session.enabled | `+++true+++` | Enables the save-session filter.
4849
|spring.cloud.gateway.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |

docs/src/main/asciidoc/spring-cloud-gateway.adoc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,6 +1598,34 @@ spring:
15981598

15991599
For a request path of `/red/blue`, this sets the path to `/blue` before making the downstream request. Note that the `$` should be replaced with `$\` because of the YAML specification.
16001600

1601+
=== The `RewriteRequestParameter` `GatewayFilter` Factory
1602+
1603+
The `RewriteRequestParameter` `GatewayFilter` factory takes a `name` parameter and a `replacement` parameter.
1604+
It will rewrite the value of the request parameter of the given `name`.
1605+
If multiple request parameters with the same `name` are set, they will be replaced with a single value.
1606+
If no request parameter is found, no changes will be made.
1607+
The following listing configures a `RewriteRequestParameter` `GatewayFilter`:
1608+
1609+
.application.yml
1610+
====
1611+
[source,yaml]
1612+
----
1613+
spring:
1614+
cloud:
1615+
gateway:
1616+
routes:
1617+
- id: rewriterequestparameter_route
1618+
uri: https://example.org
1619+
predicates:
1620+
- Path=/products
1621+
filters:
1622+
- RewriteRequestParameter=campaign,fall2023
1623+
----
1624+
====
1625+
1626+
For a request to `/products?campaign=old`, this sets the request parameter to `campaign=fall2023`.
1627+
1628+
16011629
=== The `RewriteResponseHeader` `GatewayFilter` Factory
16021630

16031631
The `RewriteResponseHeader` `GatewayFilter` factory takes `name`, `regexp`, and `replacement` parameters.

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory;
9898
import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory;
9999
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
100+
import org.springframework.cloud.gateway.filter.factory.RewriteRequestParameterGatewayFilterFactory;
100101
import org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory;
101102
import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory;
102103
import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory;
@@ -703,6 +704,12 @@ public RequestHeaderSizeGatewayFilterFactory requestHeaderSizeGatewayFilterFacto
703704
return new RequestHeaderSizeGatewayFilterFactory();
704705
}
705706

707+
@Bean
708+
@ConditionalOnEnabledFilter
709+
public RewriteRequestParameterGatewayFilterFactory rewriteRequestParameterGatewayFilterFactory() {
710+
return new RewriteRequestParameterGatewayFilterFactory();
711+
}
712+
706713
@Bean
707714
public GzipMessageBodyResolver gzipMessageBodyResolver() {
708715
return new GzipMessageBodyResolver();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2013-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.filter.factory;
18+
19+
import java.net.URI;
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.cloud.gateway.filter.GatewayFilter;
26+
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
27+
import org.springframework.http.server.reactive.ServerHttpRequest;
28+
import org.springframework.util.Assert;
29+
import org.springframework.web.server.ServerWebExchange;
30+
import org.springframework.web.util.UriComponentsBuilder;
31+
32+
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
33+
34+
/**
35+
* @author Fredrich Ombico
36+
*/
37+
public class RewriteRequestParameterGatewayFilterFactory
38+
extends AbstractGatewayFilterFactory<RewriteRequestParameterGatewayFilterFactory.Config> {
39+
40+
/**
41+
* Replacement key.
42+
*/
43+
public static final String REPLACEMENT_KEY = "replacement";
44+
45+
public RewriteRequestParameterGatewayFilterFactory() {
46+
super(Config.class);
47+
}
48+
49+
@Override
50+
public List<String> shortcutFieldOrder() {
51+
return Arrays.asList(NAME_KEY, REPLACEMENT_KEY);
52+
}
53+
54+
@Override
55+
public GatewayFilter apply(Config config) {
56+
return new GatewayFilter() {
57+
@Override
58+
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
59+
ServerHttpRequest req = exchange.getRequest();
60+
61+
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(req.getURI());
62+
if (req.getQueryParams().containsKey(config.getName())) {
63+
uriComponentsBuilder.replaceQueryParam(config.getName(), config.getReplacement());
64+
}
65+
66+
URI uri = uriComponentsBuilder.build().toUri();
67+
ServerHttpRequest request = req.mutate().uri(uri).build();
68+
69+
return chain.filter(exchange.mutate().request(request).build());
70+
}
71+
72+
@Override
73+
public String toString() {
74+
return filterToStringCreator(RewriteRequestParameterGatewayFilterFactory.this)
75+
.append(config.getName(), config.replacement).toString();
76+
}
77+
};
78+
}
79+
80+
public static class Config extends AbstractGatewayFilterFactory.NameConfig {
81+
82+
private String replacement;
83+
84+
public String getReplacement() {
85+
return replacement;
86+
}
87+
88+
public Config setReplacement(String replacement) {
89+
Assert.notNull(replacement, "replacement must not be null");
90+
this.replacement = replacement;
91+
return this;
92+
}
93+
94+
}
95+
96+
}

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory;
6565
import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory.StripVersion;
6666
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
67+
import org.springframework.cloud.gateway.filter.factory.RewriteRequestParameterGatewayFilterFactory;
6768
import org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory;
6869
import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory;
6970
import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory;
@@ -686,6 +687,17 @@ public GatewayFilterSpec rewriteLocationResponseHeader(String stripVersionMode,
686687
.setHostValue(hostValue).setProtocols(protocolsRegex)));
687688
}
688689

690+
/**
691+
* A filter that rewrites the value of a request parameter
692+
* @param name The name of the request parameter to replace
693+
* @param replacement The new value for the request parameter
694+
* @return a {@link GatewayFilterSpec} that can be used to apply additional filters
695+
*/
696+
public GatewayFilterSpec rewriteRequestParameter(String name, String replacement) {
697+
return filter(getBean(RewriteRequestParameterGatewayFilterFactory.class)
698+
.apply(c -> c.setReplacement(replacement).setName(name)));
699+
}
700+
689701
/**
690702
* A filter that sets the status on the response before it is returned to the client
691703
* by the Gateway.

spring-cloud-gateway-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@
167167
"description": "Enables the rewrite-location filter.",
168168
"defaultValue": "true"
169169
},
170+
{
171+
"name": "spring.cloud.gateway.filter.rewrite-request-parameter.enabled",
172+
"type": "java.lang.Boolean",
173+
"description": "Enables the rewrite-request-parameter filter.",
174+
"defaultValue": "true"
175+
},
170176
{
171177
"name": "spring.cloud.gateway.filter.set-status.enabled",
172178
"type": "java.lang.Boolean",

spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/conditional/DisableBuiltInFiltersTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ public void shouldInjectOnlyEnabledBuiltInFilters() {
103103
"spring.cloud.gateway.filter.rewrite-response-header.enabled=false",
104104
"spring.cloud.gateway.filter.rewrite-location-response-header.enabled=false",
105105
"spring.cloud.gateway.filter.rewrite-location.enabled=false",
106+
"spring.cloud.gateway.filter.rewrite-request-parameter.enabled=false",
106107
"spring.cloud.gateway.filter.set-status.enabled=false",
107108
"spring.cloud.gateway.filter.save-session.enabled=false",
108109
"spring.cloud.gateway.filter.strip-prefix.enabled=false",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2013-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.filter.factory;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.boot.SpringBootConfiguration;
22+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
23+
import org.springframework.boot.test.context.SpringBootTest;
24+
import org.springframework.cloud.gateway.test.BaseWebClientTests;
25+
import org.springframework.context.annotation.Import;
26+
import org.springframework.test.annotation.DirtiesContext;
27+
28+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
29+
30+
/**
31+
* @author Fredrich Ombico
32+
*/
33+
@SpringBootTest(webEnvironment = RANDOM_PORT)
34+
@DirtiesContext
35+
class RewriteRequestParameterGatewayFilterFactoryIntegrationTests extends BaseWebClientTests {
36+
37+
@Test
38+
void rewriteRequestParameterFilterWorks() {
39+
testClient.get().uri("/get?campaign=old").header("Host", "www.rewriterequestparameter.org").exchange()
40+
.expectStatus().isOk().expectBody().jsonPath("$.args.size", "fall2023");
41+
}
42+
43+
@EnableAutoConfiguration
44+
@SpringBootConfiguration
45+
@Import(DefaultTestConfig.class)
46+
public static class TestConfig {
47+
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2013-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.filter.factory;
18+
19+
import java.net.URI;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.junit.jupiter.api.Test;
24+
import org.mockito.ArgumentCaptor;
25+
import reactor.core.publisher.Mono;
26+
27+
import org.springframework.cloud.gateway.filter.GatewayFilter;
28+
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
29+
import org.springframework.http.HttpMethod;
30+
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
31+
import org.springframework.mock.web.server.MockServerWebExchange;
32+
import org.springframework.util.MultiValueMap;
33+
import org.springframework.web.server.ServerWebExchange;
34+
import org.springframework.web.util.UriComponentsBuilder;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.mockito.Mockito.mock;
38+
import static org.mockito.Mockito.when;
39+
import static org.springframework.cloud.gateway.filter.factory.RewriteRequestParameterGatewayFilterFactory.Config;
40+
41+
/**
42+
* @author Fredrich Ombico
43+
*/
44+
class RewriteRequestParameterGatewayFilterFactoryTests {
45+
46+
@Test
47+
void toStringFormat() {
48+
Config config = new Config();
49+
config.setName("campaign");
50+
config.setReplacement("fall2023");
51+
GatewayFilter filter = new RewriteRequestParameterGatewayFilterFactory().apply(config);
52+
assertThat(filter.toString()).contains("campaign").contains("fall2023");
53+
}
54+
55+
@Test
56+
void rewriteRequestParameterFilterWorks() {
57+
testRewriteRequestParameterFilter("campaign", "fall2023", "size=small&campaign=old",
58+
Map.of("size", List.of("small"), "campaign", List.of("fall2023")));
59+
}
60+
61+
@Test
62+
void rewriteRequestParameterFilterRewritesMultipleParamsWithSameName() {
63+
testRewriteRequestParameterFilter("campaign", "fall2023", "campaign=fall&size=small&campaign=old",
64+
Map.of("size", List.of("small"), "campaign", List.of("fall2023")));
65+
}
66+
67+
@Test
68+
void rewriteRequestParameterFilterDoesNotAddParamIfNameNotFound() {
69+
testRewriteRequestParameterFilter("campaign", "winter2023", "color=green&sort=popular",
70+
Map.of("color", List.of("green"), "sort", List.of("popular")));
71+
}
72+
73+
@Test
74+
void rewriteRequestParameterFilterWorksWithSpecialCharacters() {
75+
testRewriteRequestParameterFilter("campaign", "black friday~(1.A-B_C!)", "campaign=old&color=green",
76+
Map.of("campaign", List.of("black friday~(1.A-B_C!)"), "color", List.of("green")));
77+
}
78+
79+
private void testRewriteRequestParameterFilter(String name, String replacement, String query,
80+
Map<String, List<String>> expectedQueryParams) {
81+
GatewayFilter filter = new RewriteRequestParameterGatewayFilterFactory()
82+
.apply(config -> config.setReplacement(replacement).setName(name));
83+
84+
URI url = UriComponentsBuilder.fromUriString("http://localhost/get").query(query).build(true).toUri();
85+
MockServerHttpRequest request = MockServerHttpRequest.method(HttpMethod.GET, url).build();
86+
87+
ServerWebExchange exchange = MockServerWebExchange.from(request);
88+
89+
GatewayFilterChain filterChain = mock(GatewayFilterChain.class);
90+
91+
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
92+
when(filterChain.filter(captor.capture())).thenReturn(Mono.empty());
93+
94+
filter.filter(exchange, filterChain);
95+
96+
ServerWebExchange webExchange = captor.getValue();
97+
98+
MultiValueMap<String, String> actualQueryParams = webExchange.getRequest().getQueryParams();
99+
assertThat(actualQueryParams).containsExactlyInAnyOrderEntriesOf(expectedQueryParams);
100+
}
101+
102+
}

spring-cloud-gateway-server/src/test/resources/application.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ spring:
341341
- RewriteLocationResponseHeader
342342
- AddResponseHeader=Location, https://backend.org:443/v1/some/object/id
343343

344+
# =====================================
345+
- id: rewrite_request_parameter_test
346+
uri: ${test.uri}
347+
predicates:
348+
- Host=**.rewriterequestparameter.org
349+
filters:
350+
- RewriteRequestParameter=campaign,fall2023
351+
344352
# =====================================
345353
- id: rewrite_response_header_test
346354
uri: ${test.uri}

0 commit comments

Comments
 (0)