Add redirectToHttps DSL Configurer

Closes gh-16679
This commit is contained in:
Josh Cummings 2025-02-28 09:09:45 -07:00
parent 2d96fba5cf
commit be23268c37
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
8 changed files with 501 additions and 6 deletions

View File

@ -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());

View File

@ -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.
*

View File

@ -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);
}
}

View File

@ -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
*

View File

@ -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!!) }
}
}
}

View File

@ -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();
}
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}