ソースを参照

Add redirectToHttps DSL Configurer

Closes gh-16679
Josh Cummings 5 ヶ月 前
コミット
be23268c37

+ 2 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java

@@ -54,6 +54,7 @@ import org.springframework.security.web.session.ConcurrentSessionFilter;
 import org.springframework.security.web.session.DisableEncodeUrlFilter;
 import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
 import org.springframework.security.web.session.SessionManagementFilter;
+import org.springframework.security.web.transport.HttpsRedirectFilter;
 import org.springframework.web.filter.CorsFilter;
 
 /**
@@ -78,6 +79,7 @@ final class FilterOrderRegistration {
 		put(DisableEncodeUrlFilter.class, order.next());
 		put(ForceEagerSessionCreationFilter.class, order.next());
 		put(ChannelProcessingFilter.class, order.next());
+		put(HttpsRedirectFilter.class, order.next());
 		order.next(); // gh-8105
 		put(WebAsyncManagerIntegrationFilter.class, order.next());
 		put(SecurityContextHolderFilter.class, order.next());

+ 48 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@@ -59,6 +59,7 @@ import org.springframework.security.config.annotation.web.configurers.Expression
 import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
 import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
 import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer;
 import org.springframework.security.config.annotation.web.configurers.JeeConfigurer;
 import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
 import org.springframework.security.config.annotation.web.configurers.PasswordManagementConfigurer;
@@ -3145,6 +3146,53 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
 		return HttpSecurity.this;
 	}
 
+	/**
+	 * Configures channel security. In order for this configuration to be useful at least
+	 * one mapping to a required channel must be provided.
+	 *
+	 * <h2>Example Configuration</h2>
+	 *
+	 * The example below demonstrates how to require HTTPS for every request. Only
+	 * requiring HTTPS for some requests is supported, for example if you need to
+	 * differentiate between local and production deployments.
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * &#064;EnableWebSecurity
+	 * public class RequireHttpsConfig {
+	 *
+	 * 	&#064;Bean
+	 * 	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+	 * 		http
+	 * 			.authorizeHttpRequests((authorize) -&gt; authorize
+	 * 				anyRequest().authenticated()
+	 * 			)
+	 * 			.formLogin(withDefaults())
+	 * 			.redirectToHttps(withDefaults());
+	 * 		return http.build();
+	 * 	}
+	 *
+	 * 	&#064;Bean
+	 * 	public UserDetailsService userDetailsService() {
+	 * 		UserDetails user = User.withDefaultPasswordEncoder()
+	 * 			.username(&quot;user&quot;)
+	 * 			.password(&quot;password&quot;)
+	 * 			.roles(&quot;USER&quot;)
+	 * 			.build();
+	 * 		return new InMemoryUserDetailsManager(user);
+	 * 	}
+	 * }
+	 * </pre>
+	 * @param httpsRedirectConfigurerCustomizer the {@link Customizer} to provide more
+	 * options for the {@link HttpsRedirectConfigurer}
+	 * @return the {@link HttpSecurity} for further customizations
+	 */
+	public HttpSecurity redirectToHttps(
+			Customizer<HttpsRedirectConfigurer<HttpSecurity>> httpsRedirectConfigurerCustomizer) throws Exception {
+		httpsRedirectConfigurerCustomizer.customize(getOrApply(new HttpsRedirectConfigurer<>()));
+		return HttpSecurity.this;
+	}
+
 	/**
 	 * Configures HTTP Basic authentication.
 	 *

+ 76 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpsRedirectConfigurer.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2025 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
+ *
+ *      https://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.annotation.web.configurers;
+
+import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.web.PortMapper;
+import org.springframework.security.web.transport.HttpsRedirectFilter;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+
+/**
+ * Specifies for what requests the application should redirect to HTTPS. When this
+ * configurer is added, it redirects all HTTP requests by default to HTTPS.
+ *
+ * <h2>Security Filters</h2>
+ *
+ * The following Filters are populated
+ *
+ * <ul>
+ * <li>{@link HttpsRedirectFilter}</li>
+ * </ul>
+ *
+ * <h2>Shared Objects Created</h2>
+ *
+ * No shared objects are created.
+ *
+ * <h2>Shared Objects Used</h2>
+ *
+ * The following shared objects are used:
+ *
+ * <ul>
+ * <li>{@link PortMapper} is used to configure {@link HttpsRedirectFilter}</li>
+ * </ul>
+ *
+ * @param <H> the type of {@link HttpSecurityBuilder} that is being configured
+ * @author Josh Cummings
+ * @since 6.5
+ */
+public final class HttpsRedirectConfigurer<H extends HttpSecurityBuilder<H>>
+		extends AbstractHttpConfigurer<HeadersConfigurer<H>, H> {
+
+	private RequestMatcher requestMatcher;
+
+	public HttpsRedirectConfigurer<H> requestMatchers(RequestMatcher... matchers) {
+		this.requestMatcher = new OrRequestMatcher(matchers);
+		return this;
+	}
+
+	@Override
+	public void configure(H http) throws Exception {
+		HttpsRedirectFilter filter = new HttpsRedirectFilter();
+		if (this.requestMatcher != null) {
+			filter.setRequestMatcher(this.requestMatcher);
+		}
+		PortMapper mapper = http.getSharedObject(PortMapper.class);
+		if (mapper != null) {
+			filter.setPortMapper(mapper);
+		}
+		http.addFilter(filter);
+	}
+
+}

+ 33 - 0
config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpSecurityDsl.kt

@@ -533,6 +533,39 @@ class HttpSecurityDsl(private val http: HttpSecurity, private val init: HttpSecu
         this.http.requiresChannel(requiresChannelCustomizer)
     }
 
+    /**
+     * Configures channel security. In order for this configuration to be useful at least
+     * one mapping to a required channel must be provided.
+     *
+     * Example:
+     *
+     * The example below demonstrates how to require HTTPS for every request. Only
+     * requiring HTTPS for some requests is supported, for example if you need to differentiate
+     * between local and production deployments.
+     *
+     * ```
+     * @Configuration
+     * @EnableWebSecurity
+     * class RequireHttpsConfig {
+     *
+     * 	@Bean
+     * 	fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+     * 		http {
+     * 			redirectToHttps { }
+     * 		}
+     * 		return http.build();
+     * 	}
+     * }
+     * ```
+     * @param httpsRedirectConfiguration custom configuration to apply to HTTPS redirect rules
+     * @see [HttpsRedirectDsl]
+     * @since 6.5
+     */
+    fun redirectToHttps(httpsRedirectConfiguration: HttpsRedirectDsl.() -> Unit) {
+        val httpsRedirectCustomizer = HttpsRedirectDsl().apply(httpsRedirectConfiguration).get()
+        this.http.redirectToHttps(httpsRedirectCustomizer)
+    }
+
     /**
      * Adds X509 based pre authentication to an application
      *

+ 41 - 0
config/src/main/kotlin/org/springframework/security/config/annotation/web/HttpsRedirectDsl.kt

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2002-2020 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
+ *
+ *      https://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.annotation.web
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configurers.HttpsRedirectConfigurer
+import org.springframework.security.web.PortMapper
+import org.springframework.security.web.util.matcher.RequestMatcher
+
+/**
+ * A Kotlin DSL to configure [ServerHttpSecurity] HTTPS redirection rules using idiomatic
+ * Kotlin code.
+ *
+ * @author Eleftheria Stein
+ * @since 5.4
+ * @property portMapper the [PortMapper] that specifies a custom HTTPS port to redirect to.
+ */
+@SecurityMarker
+class HttpsRedirectDsl {
+    var requestMatchers: Array<out RequestMatcher>? = null
+
+    internal fun get(): (HttpsRedirectConfigurer<HttpSecurity>) -> Unit {
+        return { https ->
+            requestMatchers?.also { https.requestMatchers(*requestMatchers!!) }
+        }
+    }
+}

+ 169 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpsRedirectConfigurerTests.java

@@ -0,0 +1,169 @@
+/*
+ * Copyright 2002-2019 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
+ *
+ *      https://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.annotation.web.configurers;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.test.SpringTestContext;
+import org.springframework.security.config.test.SpringTestContextExtension;
+import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean;
+import org.springframework.security.web.PortMapper;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.springframework.security.config.Customizer.withDefaults;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link HttpsRedirectConfigurerTests}
+ *
+ * @author Josh Cummings
+ */
+@ExtendWith(SpringTestContextExtension.class)
+public class HttpsRedirectConfigurerTests {
+
+	public final SpringTestContext spring = new SpringTestContext(this);
+
+	@Autowired
+	MockMvc mvc;
+
+	@Test
+	public void getWhenSecureThenDoesNotRedirect() throws Exception {
+		this.spring.register(RedirectToHttpConfig.class).autowire();
+		// @formatter:off
+		this.mvc.perform(get("https://localhost"))
+				.andExpect(status().isNotFound());
+		// @formatter:on
+	}
+
+	@Test
+	public void getWhenInsecureThenRespondsWithRedirectToSecure() throws Exception {
+		this.spring.register(RedirectToHttpConfig.class).autowire();
+		// @formatter:off
+		this.mvc.perform(get("http://localhost"))
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("https://localhost"));
+		// @formatter:on
+	}
+
+	@Test
+	public void getWhenInsecureAndPathRequiresTransportSecurityThenRedirects() throws Exception {
+		this.spring.register(SometimesRedirectToHttpsConfig.class, UsePathPatternConfig.class).autowire();
+		// @formatter:off
+		this.mvc.perform(get("http://localhost:8080"))
+				.andExpect(status().isNotFound());
+		this.mvc.perform(get("http://localhost:8080/secure"))
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("https://localhost:8443/secure"));
+		// @formatter:on
+	}
+
+	@Test
+	public void getWhenInsecureAndUsingCustomPortMapperThenRespondsWithRedirectToSecurePort() throws Exception {
+		this.spring.register(RedirectToHttpsViaCustomPortsConfig.class).autowire();
+		PortMapper portMapper = this.spring.getContext().getBean(PortMapper.class);
+		given(portMapper.lookupHttpsPort(4080)).willReturn(4443);
+		// @formatter:off
+		this.mvc.perform(get("http://localhost:4080"))
+				.andExpect(status().isFound())
+				.andExpect(redirectedUrl("https://localhost:4443"));
+		// @formatter:on
+	}
+
+	@Configuration
+	@EnableWebMvc
+	@EnableWebSecurity
+	static class RedirectToHttpConfig {
+
+		@Bean
+		SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.redirectToHttps(withDefaults());
+			// @formatter:on
+			return http.build();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebMvc
+	@EnableWebSecurity
+	static class SometimesRedirectToHttpsConfig {
+
+		@Bean
+		SecurityFilterChain springSecurity(HttpSecurity http, PathPatternRequestMatcher.Builder path) throws Exception {
+			// @formatter:off
+			http
+				.redirectToHttps((https) -> https.requestMatchers(path.matcher("/secure")));
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
+			return new PathPatternRequestMatcherBuilderFactoryBean();
+		}
+
+	}
+
+	@Configuration
+	@EnableWebMvc
+	@EnableWebSecurity
+	static class RedirectToHttpsViaCustomPortsConfig {
+
+		@Bean
+		SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
+			// @formatter:off
+			http
+				.portMapper((p) -> p.portMapper(portMapper()))
+				.redirectToHttps(withDefaults());
+
+			// @formatter:on
+			return http.build();
+		}
+
+		@Bean
+		PortMapper portMapper() {
+			return mock(PortMapper.class);
+		}
+
+	}
+
+	@Configuration
+	static class UsePathPatternConfig {
+
+		@Bean
+		PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() {
+			return new PathPatternRequestMatcherBuilderFactoryBean();
+		}
+
+	}
+
+}

+ 130 - 0
config/src/test/kotlin/org/springframework/security/config/annotation/web/HttpsRedirectDslTests.kt

@@ -0,0 +1,130 @@
+/*
+ * Copyright 2002-2020 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
+ *
+ *      https://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.annotation.web
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpHeaders
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.test.SpringTestContext
+import org.springframework.security.config.test.SpringTestContextExtension
+import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean
+import org.springframework.security.web.PortMapperImpl
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher
+import org.springframework.test.web.servlet.MockMvc
+import org.springframework.test.web.servlet.get
+import org.springframework.web.servlet.config.annotation.EnableWebMvc
+import java.net.URI
+import java.util.*
+
+/**
+ * Tests for [HttpsRedirectDsl]
+ *
+ * @author Eleftheria Stein
+ */
+@ExtendWith(SpringTestContextExtension::class)
+class HttpsRedirectDslTests {
+    @JvmField
+    val spring = SpringTestContext(this)
+
+    @Autowired
+    lateinit var mockMvc: MockMvc
+
+    @Test
+    fun `request when matches redirect to HTTPS matcher then redirects to HTTPS`() {
+        this.spring.register(HttpRedirectMatcherConfig::class.java, UsePathPatternConfig::class.java).autowire()
+
+        val result = this.mockMvc.get("/secure")
+            .andExpect {
+                status { is3xxRedirection() }
+            }.andReturn()
+
+        val location = result.response.getHeader(HttpHeaders.LOCATION)?.let { URI.create(it) }
+        assertThat(location?.scheme).isEqualTo("https")
+    }
+
+    @Test
+    fun `request when does not match redirect to HTTPS matcher then does not redirect`() {
+        this.spring.register(HttpRedirectMatcherConfig::class.java, UsePathPatternConfig::class.java).autowire()
+
+        this.mockMvc.get("/")
+            .andExpect {
+                status { isNotFound() }
+            }
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class HttpRedirectMatcherConfig {
+        @Bean
+        open fun springFilterChain(http: HttpSecurity, path: PathPatternRequestMatcher.Builder): SecurityFilterChain {
+            http {
+                redirectToHttps {
+                    requestMatchers = arrayOf(path.matcher("/secure"))
+                }
+            }
+            return http.build()
+        }
+    }
+
+    @Configuration
+    open class UsePathPatternConfig {
+        @Bean
+        open fun requestMatcherBuilder() = PathPatternRequestMatcherBuilderFactoryBean()
+    }
+
+    @Test
+    fun `request when port mapper configured then redirected to HTTPS port`() {
+        this.spring.register(PortMapperConfig::class.java).autowire()
+
+        val result = this.mockMvc.get("http://localhost:543")
+            .andExpect {
+                status {
+                    is3xxRedirection()
+                }
+            }.andReturn()
+
+        val location = result.response.getHeader(HttpHeaders.LOCATION)?.let { URI.create(it) }
+        assertThat(location?.scheme).isEqualTo("https")
+        assertThat(location?.port).isEqualTo(123)
+    }
+
+    @Configuration
+    @EnableWebSecurity
+    @EnableWebMvc
+    open class PortMapperConfig {
+        @Bean
+        open fun springFilterChain(http: HttpSecurity): SecurityFilterChain {
+            val customPortMapper = PortMapperImpl()
+            customPortMapper.setPortMappings(Collections.singletonMap("543", "123"))
+            http {
+                portMapper {
+                    portMapper = customPortMapper
+                }
+                redirectToHttps { }
+            }
+            return http.build()
+        }
+    }
+}

+ 2 - 6
docs/modules/ROOT/pages/servlet/exploits/http.adoc

@@ -27,9 +27,7 @@ public class WebSecurityConfig {
 	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
 		http
 			// ...
-			.requiresChannel(channel -> channel
-				.anyRequest().requiresSecure()
-			);
+			.redirectToHttps(withDefaults());
 		return http.build();
 	}
 }
@@ -47,9 +45,7 @@ class SecurityConfig {
     open fun filterChain(http: HttpSecurity): SecurityFilterChain {
         http {
             // ...
-            requiresChannel {
-                secure(AnyRequestMatcher.INSTANCE, "REQUIRES_SECURE_CHANNEL")
-            }
+            redirectToHttps { }
         }
         return http.build()
     }