Skip to content

Commit 2f23bc3

Browse files
gregturnspencergibb
authored andcommitted
Add filter to force WebSession::save. (#166)
In some scenarios, such as making a remote call while using Spring Session between two processes, forcing a WebSession::save before making the call ensures that session data is in place on the other side. Resolves #58.
1 parent 6fe7207 commit 2f23bc3

File tree

6 files changed

+168
-1
lines changed

6 files changed

+168
-1
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,27 @@ spring:
509509

510510
For a request path of `/foo/bar`, this will set the path to `/bar` before making the downstream request. Notice the `$\` which is replaced with `$` because of the YAML spec.
511511

512+
=== SaveSession GatewayFilter Factory
513+
The SaveSession GatewayFilter Factory forces a `WebSession::save` operation _before_ forwarding the call downstream. This is of particular use when
514+
using something like http://projects.spring.io/spring-session/[Spring Session] with a lazy data store and need to ensure the session state has been saved before making the forwarded call.
515+
516+
.application.yml
517+
[source,yaml]
518+
----
519+
spring:
520+
cloud:
521+
gateway:
522+
routes:
523+
- id: save_session
524+
uri: http://example.org
525+
predicates:
526+
- Path=/foo/**
527+
filters:
528+
- SaveSession
529+
----
530+
531+
If you are integrating http://projects.spring.io/spring-security/[Spring Security] with Spring Session, and want to ensure security details have been forwarded to the remote process, this is critical.
532+
512533
=== SecureHeaders GatewayFilter Factory
513534
The SecureHeaders GatewayFilter Factory adds a number of headers to the response at the reccomendation from https://blog.appcanary.com/2017/http-security-headers.html[this blog post].
514535

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory;
5555
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
5656
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
57+
import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory;
5758
import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory;
5859
import org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties;
5960
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory;
@@ -101,7 +102,6 @@
101102
import reactor.core.publisher.Flux;
102103
import reactor.ipc.netty.http.client.HttpClient;
103104
import reactor.ipc.netty.http.client.HttpClientOptions;
104-
import reactor.ipc.netty.http.client.HttpClientRequest;
105105
import reactor.ipc.netty.resources.PoolResources;
106106
import rx.RxReactiveStreams;
107107

@@ -404,6 +404,10 @@ public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() {
404404
return new SetStatusGatewayFilterFactory();
405405
}
406406

407+
@Bean
408+
public SaveSessionGatewayFilterFactory saveSessionGatewayFilterFactory() {
409+
return new SaveSessionGatewayFilterFactory();
410+
}
407411

408412
@ManagementContextConfiguration
409413
@ConditionalOnProperty(value = "management.gateway.enabled", matchIfMissing = true)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2018 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+
* http://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+
package org.springframework.cloud.gateway.filter.factory;
17+
18+
import org.springframework.cloud.gateway.filter.GatewayFilter;
19+
import org.springframework.tuple.Tuple;
20+
import org.springframework.web.server.WebSession;
21+
22+
/**
23+
* Save the current {@link WebSession} before executing the rest of the {@link org.springframework.cloud.gateway.filter.GatewayFilterChain}.
24+
*
25+
* Filter is very useful for situation where the WebSession is lazy (e.g. Spring Session MongoDB) and making a remote call requires
26+
* that {@link WebSession#save()} be called before the remote call is made.
27+
*
28+
* @author Greg Turnquist
29+
*/
30+
public class SaveSessionGatewayFilterFactory implements GatewayFilterFactory {
31+
32+
@Override
33+
public GatewayFilter apply(Tuple args) {
34+
return (exchange, chain) -> exchange.getSession()
35+
.map(WebSession::save)
36+
.then(chain.filter(exchange));
37+
}
38+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory;
3838
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
3939
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
40+
import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory;
4041
import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory;
4142
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory;
4243
import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory;
@@ -197,4 +198,8 @@ public GatewayFilterSpec setStatus(String status) {
197198
public GatewayFilterSpec setStatus(HttpStatus status) {
198199
return filter(getBean(SetStatusGatewayFilterFactory.class).apply(status));
199200
}
201+
202+
public GatewayFilterSpec saveSession() {
203+
return filter(getBean(SaveSessionGatewayFilterFactory.class).apply(EMPTY_TUPLE));
204+
}
200205
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2018 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+
* http://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+
package org.springframework.cloud.gateway.filter.factory;
17+
18+
import java.time.Duration;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
import org.springframework.boot.SpringBootConfiguration;
25+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
26+
import org.springframework.boot.test.context.SpringBootTest;
27+
import org.springframework.cloud.gateway.test.BaseWebClientTests;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Import;
30+
import org.springframework.test.annotation.DirtiesContext;
31+
import org.springframework.test.context.ActiveProfiles;
32+
import org.springframework.test.context.junit4.SpringRunner;
33+
import org.springframework.web.server.WebSession;
34+
import org.springframework.web.server.session.WebSessionManager;
35+
import reactor.core.publisher.Mono;
36+
import reactor.test.StepVerifier;
37+
38+
import static org.mockito.Mockito.mock;
39+
import static org.mockito.Mockito.verify;
40+
import static org.mockito.Mockito.when;
41+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
42+
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
43+
44+
/**
45+
* @author Greg Turnquist
46+
*/
47+
@RunWith(SpringRunner.class)
48+
@SpringBootTest(webEnvironment = RANDOM_PORT)
49+
@DirtiesContext
50+
@ActiveProfiles(profiles = "save-session-web-filter")
51+
public class SaveSessionGatewayFilterFactoryTests extends BaseWebClientTests {
52+
53+
static WebSession mockWebSession = mock(WebSession.class);
54+
55+
@Test
56+
public void webCallShouldTriggerWebSessionSaveAction() {
57+
58+
when(mockWebSession.getAttributes()).thenReturn(new HashMap<>());
59+
when(mockWebSession.save()).thenReturn(Mono.empty());
60+
61+
Mono<Map> result = webClient.get()
62+
.uri("/get")
63+
.exchange()
64+
.flatMap(response -> response.body(toMono(Map.class)));
65+
66+
StepVerifier.create(result)
67+
.consumeNextWith(response -> {/* Don't care about data, just need to catch signal */})
68+
.expectComplete()
69+
.verify(Duration.ofMinutes(10));
70+
71+
verify(mockWebSession).save();
72+
}
73+
74+
@EnableAutoConfiguration
75+
@SpringBootConfiguration
76+
@Import(DefaultTestConfig.class)
77+
static class TestConfig {
78+
79+
@Bean
80+
WebSessionManager webSessionManager() {
81+
return exchange -> Mono.just(mockWebSession);
82+
}
83+
}
84+
85+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
test:
2+
hostport: httpbin.org:80
3+
uri: lb://testservice
4+
5+
spring:
6+
cloud:
7+
gateway:
8+
routes:
9+
- id: save_session_test
10+
uri: ${test.uri}
11+
predicates:
12+
- Path=/get
13+
filters:
14+
- SaveSession

0 commit comments

Comments
 (0)