diff --git a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java index 3c3d8acf9c..a272ae7363 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java +++ b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java @@ -23,6 +23,10 @@ package org.springframework.security.config.web.server; public enum SecurityWebFiltersOrder { FIRST(Integer.MIN_VALUE), HTTP_HEADERS_WRITER, + /** + * {@link org.springframework.security.web.server.transport.HttpsRedirectWebFilter} + */ + HTTPS_REDIRECT, /** * {@link org.springframework.web.cors.reactive.CorsWebFilter} */ diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index f344332186..8f2b790ddd 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import reactor.core.publisher.Mono; +import reactor.util.context.Context; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; @@ -68,6 +69,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtRea import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler; import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint; import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter; +import org.springframework.security.web.PortMapper; import org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint; import org.springframework.security.web.server.MatcherSecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain; @@ -115,11 +117,13 @@ import org.springframework.security.web.server.savedrequest.NoOpServerRequestCac import org.springframework.security.web.server.savedrequest.ServerRequestCache; import org.springframework.security.web.server.savedrequest.ServerRequestCacheWebFilter; import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache; +import org.springframework.security.web.server.transport.HttpsRedirectWebFilter; import org.springframework.security.web.server.ui.LoginPageGeneratingWebFilter; import org.springframework.security.web.server.ui.LogoutPageGeneratingWebFilter; import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.MediaTypeServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry; @@ -133,7 +137,6 @@ import org.springframework.web.cors.reactive.DefaultCorsProcessor; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; -import reactor.util.context.Context; import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry; import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match; @@ -199,6 +202,8 @@ public class ServerHttpSecurity { private AuthorizeExchangeSpec authorizeExchange; + private HttpsRedirectSpec httpsRedirectSpec; + private HeaderSpec headers = new HeaderSpec(); private CsrfSpec csrf = new CsrfSpec(); @@ -286,6 +291,42 @@ public class ServerHttpSecurity { return this; } + /** + * Configures HTTPS redirection rules. If the default is used: + * + *
+ * @Bean
+ * public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+ * http
+ * // ...
+ * .redirectToHttps();
+ * return http.build();
+ * }
+ *
+ *
+ * Then all non-HTTPS requests will be redirected to HTTPS.
+ *
+ * Typically, all requests should be HTTPS; however, the focus for redirection can also be narrowed:
+ *
+ *
+ * @Bean
+ * public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+ * http
+ * // ...
+ * .redirectToHttps()
+ * .httpsRedirectWhen(serverWebExchange ->
+ * serverWebExchange.getRequest().getHeaders().containsKey("X-Requires-Https"))
+ * return http.build();
+ * }
+ *
+ *
+ * @return the {@link HttpsRedirectSpec} to customize
+ */
+ public HttpsRedirectSpec redirectToHttps() {
+ this.httpsRedirectSpec = new HttpsRedirectSpec();
+ return this.httpsRedirectSpec;
+ }
+
/**
* Configures CSRF Protection
* which is enabled by default. You can disable it using:
@@ -1044,6 +1085,9 @@ public class ServerHttpSecurity {
if (securityContextRepositoryWebFilter != null) {
this.webFilters.add(securityContextRepositoryWebFilter);
}
+ if (this.httpsRedirectSpec != null) {
+ this.httpsRedirectSpec.configure(this);
+ }
if (this.csrf != null) {
this.csrf.configure(this);
}
@@ -1277,6 +1321,61 @@ public class ServerHttpSecurity {
}
}
+ /**
+ * Configures HTTPS redirection rules
+ *
+ * @author Josh Cummings
+ * @since 5.1
+ * @see #redirectToHttps()
+ */
+ public class HttpsRedirectSpec {
+ private ServerWebExchangeMatcher serverWebExchangeMatcher;
+ private PortMapper portMapper;
+
+ /**
+ * Configures when this filter should redirect to https
+ *
+ * By default, the filter will redirect whenever an exchange's scheme is not https
+ *
+ * @param matchers the list of conditions that, when any are met, the filter should redirect to https
+ * @return the {@link HttpsRedirectSpec} for additional configuration
+ */
+ public HttpsRedirectSpec httpsRedirectWhen(ServerWebExchangeMatcher... matchers) {
+ this.serverWebExchangeMatcher = new OrServerWebExchangeMatcher(matchers);
+ return this;
+ }
+
+ /**
+ * Configures a custom HTTPS port to redirect to
+ *
+ * @param portMapper the {@link PortMapper} to use
+ * @return the {@link HttpsRedirectSpec} for additional configuration
+ */
+ public HttpsRedirectSpec portMapper(PortMapper portMapper) {
+ this.portMapper = portMapper;
+ return this;
+ }
+
+ protected void configure(ServerHttpSecurity http) {
+ HttpsRedirectWebFilter httpsRedirectWebFilter = new HttpsRedirectWebFilter();
+ if (this.serverWebExchangeMatcher != null) {
+ httpsRedirectWebFilter.setRequiresHttpsRedirectMatcher(this.serverWebExchangeMatcher);
+ }
+ if (this.portMapper != null) {
+ httpsRedirectWebFilter.setPortMapper(this.portMapper);
+ }
+ http.addFilterAt(httpsRedirectWebFilter, SecurityWebFiltersOrder.HTTPS_REDIRECT);
+ }
+
+ /**
+ * Allows method chaining to continue configuring the {@link ServerHttpSecurity}
+ * @return the {@link ServerHttpSecurity} to continue configuring
+ */
+ public ServerHttpSecurity and() {
+ return ServerHttpSecurity.this;
+ }
+ }
+
/**
* Configures CSRF Protection
*
diff --git a/config/src/test/java/org/springframework/security/config/web/server/HttpsRedirectSpecTests.java b/config/src/test/java/org/springframework/security/config/web/server/HttpsRedirectSpecTests.java
new file mode 100644
index 0000000000..bba6846af7
--- /dev/null
+++ b/config/src/test/java/org/springframework/security/config/web/server/HttpsRedirectSpecTests.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2002-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.security.config.web.server;
+
+import org.apache.http.HttpHeaders;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.web.PortMapper;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.config.EnableWebFlux;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link HttpsRedirectSpecTests}
+ *
+ * @author Josh Cummings
+ */
+public class HttpsRedirectSpecTests {
+ @Rule
+ public final SpringTestRule spring = new SpringTestRule();
+
+ WebTestClient client;
+
+ @Autowired
+ public void setApplicationContext(ApplicationContext context) {
+ this.client = WebTestClient.bindToApplicationContext(context).build();
+ }
+
+ @Test
+ public void getWhenSecureThenDoesNotRedirect() {
+ this.spring.register(RedirectToHttpConfig.class).autowire();
+
+ this.client.get()
+ .uri("https://localhost")
+ .exchange()
+ .expectStatus().isNotFound();
+ }
+
+ @Test
+ public void getWhenInsecureThenRespondsWithRedirectToSecure() {
+ this.spring.register(RedirectToHttpConfig.class).autowire();
+
+ this.client.get()
+ .uri("http://localhost")
+ .exchange()
+ .expectStatus().isFound()
+ .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost");
+ }
+
+ @Test
+ public void getWhenInsecureAndPathRequiresTransportSecurityThenRedirects() {
+ this.spring.register(SometimesRedirectToHttpsConfig.class).autowire();
+
+ this.client.get()
+ .uri("http://localhost:8080")
+ .exchange()
+ .expectStatus().isNotFound();
+
+ this.client.get()
+ .uri("http://localhost:8080/secure")
+ .exchange()
+ .expectStatus().isFound()
+ .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost:8443/secure");
+ }
+
+ @Test
+ public void getWhenInsecureAndUsingCustomPortMapperThenRespondsWithRedirectToSecurePort() {
+ this.spring.register(RedirectToHttpsViaCustomPortsConfig.class).autowire();
+
+ PortMapper portMapper = this.spring.getContext().getBean(PortMapper.class);
+ when(portMapper.lookupHttpsPort(4080)).thenReturn(4443);
+
+ this.client.get()
+ .uri("http://localhost:4080")
+ .exchange()
+ .expectStatus().isFound()
+ .expectHeader().valueEquals(HttpHeaders.LOCATION, "https://localhost:4443");
+ }
+
+ @EnableWebFlux
+ @EnableWebFluxSecurity
+ static class RedirectToHttpConfig {
+ @Bean
+ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+ // @formatter:off
+ http
+ .redirectToHttps();
+ // @formatter:on
+
+ return http.build();
+ }
+ }
+
+ @EnableWebFlux
+ @EnableWebFluxSecurity
+ static class SometimesRedirectToHttpsConfig {
+ @Bean
+ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+ // @formatter:off
+ http
+ .redirectToHttps()
+ .httpsRedirectWhen(new PathPatternParserServerWebExchangeMatcher("/secure"));
+ // @formatter:on
+
+ return http.build();
+ }
+ }
+
+ @EnableWebFlux
+ @EnableWebFluxSecurity
+ static class RedirectToHttpsViaCustomPortsConfig {
+ @Bean
+ SecurityWebFilterChain springSecurity(ServerHttpSecurity http) {
+ // @formatter:off
+ http
+ .redirectToHttps()
+ .portMapper(portMapper());
+ // @formatter:on
+
+ return http.build();
+ }
+
+ @Bean
+ public PortMapper portMapper() {
+ return mock(PortMapper.class);
+ }
+ }
+}
diff --git a/web/src/main/java/org/springframework/security/web/server/transport/HttpsRedirectWebFilter.java b/web/src/main/java/org/springframework/security/web/server/transport/HttpsRedirectWebFilter.java
new file mode 100644
index 0000000000..059b423085
--- /dev/null
+++ b/web/src/main/java/org/springframework/security/web/server/transport/HttpsRedirectWebFilter.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2002-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.security.web.server.transport;
+
+import java.net.URI;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.web.PortMapper;
+import org.springframework.security.web.PortMapperImpl;
+import org.springframework.security.web.server.DefaultServerRedirectStrategy;
+import org.springframework.security.web.server.ServerRedirectStrategy;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.anyExchange;
+
+/**
+ * Redirects any non-HTTPS request to its HTTPS equivalent.
+ *
+ * Can be configured to use a {@link ServerWebExchangeMatcher} to narrow which requests get redirected.
+ *
+ * Can also be configured for custom ports using {@link PortMapper}.
+ *
+ * @author Josh Cummings
+ * @since 5.1
+ */
+public final class HttpsRedirectWebFilter implements WebFilter {
+ private PortMapper portMapper = new PortMapperImpl();
+
+ private ServerWebExchangeMatcher requiresHttpsRedirectMatcher = anyExchange();
+
+ private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Mono