Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/src/main/asciidoc/spring-cloud-gateway.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,27 @@ spring:

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.

=== SaveSession GatewayFilter Factory
The SaveSession GatewayFilter Factory forces a `WebSession::save` operation _before_ forwarding the call downstream. This is of particular use when
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.

.application.yml
[source,yaml]
----
spring:
cloud:
gateway:
routes:
- id: save_session
uri: http://example.org
predicates:
- Path=/foo/**
filters:
- SaveSession
----

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gregturn

If you are integrating Spring Security with Spring Session, ..., this is critical

In this part, does it mean that adding SaveSession filter is very important and it have to be applied, or adding this filter is dangerous and it should be avoided?


=== SecureHeaders GatewayFilter Factory
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].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties;
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory;
Expand Down Expand Up @@ -101,7 +102,6 @@
import reactor.core.publisher.Flux;
import reactor.ipc.netty.http.client.HttpClient;
import reactor.ipc.netty.http.client.HttpClientOptions;
import reactor.ipc.netty.http.client.HttpClientRequest;
import reactor.ipc.netty.resources.PoolResources;
import rx.RxReactiveStreams;

Expand Down Expand Up @@ -404,6 +404,10 @@ public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() {
return new SetStatusGatewayFilterFactory();
}

@Bean
public SaveSessionGatewayFilterFactory saveSessionGatewayFilterFactory() {
return new SaveSessionGatewayFilterFactory();
}

@ManagementContextConfiguration
@ConditionalOnProperty(value = "management.gateway.enabled", matchIfMissing = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.filter.factory;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.tuple.Tuple;
import org.springframework.web.server.WebSession;

/**
* Save the current {@link WebSession} before executing the rest of the {@link org.springframework.cloud.gateway.filter.GatewayFilterChain}.
*
* Filter is very useful for situation where the WebSession is lazy (e.g. Spring Session MongoDB) and making a remote call requires
* that {@link WebSession#save()} be called before the remote call is made.
*
* @author Greg Turnquist
*/
public class SaveSessionGatewayFilterFactory implements GatewayFilterFactory {

@Override
public GatewayFilter apply(Tuple args) {
return (exchange, chain) -> exchange.getSession()
.map(WebSession::save)
.then(chain.filter(exchange));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory;
Expand Down Expand Up @@ -197,4 +198,8 @@ public GatewayFilterSpec setStatus(String status) {
public GatewayFilterSpec setStatus(HttpStatus status) {
return filter(getBean(SetStatusGatewayFilterFactory.class).apply(status));
}

public GatewayFilterSpec saveSession() {
return filter(getBean(SaveSessionGatewayFilterFactory.class).apply(EMPTY_TUPLE));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.gateway.filter.factory;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.server.WebSession;
import org.springframework.web.server.session.WebSessionManager;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;

/**
* @author Greg Turnquist
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
@ActiveProfiles(profiles = "save-session-web-filter")
public class SaveSessionGatewayFilterFactoryTests extends BaseWebClientTests {

static WebSession mockWebSession = mock(WebSession.class);

@Test
public void webCallShouldTriggerWebSessionSaveAction() {

when(mockWebSession.getAttributes()).thenReturn(new HashMap<>());
when(mockWebSession.save()).thenReturn(Mono.empty());

Mono<Map> result = webClient.get()
.uri("/get")
.exchange()
.flatMap(response -> response.body(toMono(Map.class)));

StepVerifier.create(result)
.consumeNextWith(response -> {/* Don't care about data, just need to catch signal */})
.expectComplete()
.verify(Duration.ofMinutes(10));

verify(mockWebSession).save();
}

@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
static class TestConfig {

@Bean
WebSessionManager webSessionManager() {
return exchange -> Mono.just(mockWebSession);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
test:
hostport: httpbin.org:80
uri: lb://testservice

spring:
cloud:
gateway:
routes:
- id: save_session_test
uri: ${test.uri}
predicates:
- Path=/get
filters:
- SaveSession