Skip to content

Commit 3805a68

Browse files
committed
Provide MvcRequestMatcher.Builder that correctly configures the servlet path
When only one `DispatcherServlet` servlet mapping, publish `MvcRequestMatcher.Builder` bean Closes #37917
1 parent 7ac69be commit 3805a68

File tree

3 files changed

+218
-0
lines changed

3 files changed

+218
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2012-2024 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+
* https://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+
17+
package org.springframework.boot.autoconfigure.security.servlet;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
22+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
23+
import org.springframework.boot.autoconfigure.AutoConfiguration;
24+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
25+
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
26+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
27+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
28+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
29+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
30+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
32+
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
33+
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
34+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
35+
import org.springframework.context.annotation.Bean;
36+
import org.springframework.context.annotation.ConditionContext;
37+
import org.springframework.context.annotation.Conditional;
38+
import org.springframework.context.annotation.Configuration;
39+
import org.springframework.core.Ordered;
40+
import org.springframework.core.annotation.Order;
41+
import org.springframework.core.type.AnnotatedTypeMetadata;
42+
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
43+
import org.springframework.security.web.util.matcher.RequestMatcher;
44+
import org.springframework.web.servlet.DispatcherServlet;
45+
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
46+
47+
/**
48+
* {@link EnableAutoConfiguration Auto-configuration} for {@link RequestMatcher}.
49+
*
50+
* @author Wang Zhiyang
51+
* @since 3.3.0
52+
*/
53+
@AutoConfiguration(after = SecurityAutoConfiguration.class)
54+
@ConditionalOnWebApplication(type = Type.SERVLET)
55+
public class SecurityRequestMatcherAutoConfiguration {
56+
57+
@Configuration(proxyBeanMethods = false)
58+
@ConditionalOnClass(DispatcherServlet.class)
59+
@EnableConfigurationProperties(WebMvcProperties.class)
60+
public static class SecurityMvcRequestMatcherConfiguration {
61+
62+
@Bean
63+
@ConditionalOnClass({ DispatcherServlet.class, HandlerMappingIntrospector.class })
64+
@Conditional(ExactlyOneDispatcherServletCondition.class)
65+
@ConditionalOnBean(HandlerMappingIntrospector.class)
66+
@ConditionalOnMissingBean
67+
MvcRequestMatcher.Builder mvcRequestMatcherBuilder(HandlerMappingIntrospector introspector,
68+
WebMvcProperties webMvcProperties) {
69+
String servletPath = webMvcProperties.getServlet().getPath();
70+
MvcRequestMatcher.Builder mvc = new MvcRequestMatcher.Builder(introspector);
71+
return ("/".equals(servletPath)) ? mvc : mvc.servletPath(servletPath);
72+
}
73+
74+
}
75+
76+
@Order(Ordered.LOWEST_PRECEDENCE - 10)
77+
private static final class ExactlyOneDispatcherServletCondition extends SpringBootCondition {
78+
79+
@Override
80+
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
81+
82+
ConditionMessage.Builder message = ConditionMessage.forCondition("Exactly One DispatcherServlet");
83+
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
84+
List<String> dispatchServletBeans = Arrays.asList(beanFactory.getBeanNamesForType(DispatcherServlet.class));
85+
List<String> dispatchServletSingletonBeans = Arrays
86+
.asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false));
87+
if (dispatchServletSingletonBeans.size() < dispatchServletBeans.size()) {
88+
return ConditionOutcome.noMatch(message.foundExactly("scope not singleton bean"));
89+
}
90+
else if (dispatchServletSingletonBeans.size() == 1 && dispatchServletBeans.size() == 1) {
91+
return ConditionOutcome.match(message.found("single bean").items(dispatchServletBeans.get(0)));
92+
}
93+
else if (dispatchServletSingletonBeans.isEmpty()) {
94+
return ConditionOutcome
95+
.noMatch(message.foundExactly("non dispatcher servlet bean that scope is singleton"));
96+
}
97+
else {
98+
return ConditionOutcome
99+
.noMatch(message.found("multiple dispatcher servlet bean that scope is singleton")
100+
.items(dispatchServletSingletonBeans));
101+
}
102+
}
103+
104+
}
105+
106+
}

spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguratio
107107
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
108108
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
109109
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
110+
org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherAutoConfiguration
110111
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration
111112
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration
112113
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2012-2024 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+
* https://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+
17+
package org.springframework.boot.autoconfigure.security.servlet;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.boot.autoconfigure.AutoConfigurations;
22+
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
23+
import org.springframework.boot.autoconfigure.security.servlet.SecurityRequestMatcherAutoConfiguration.SecurityMvcRequestMatcherConfiguration;
24+
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
25+
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.context.annotation.Scope;
29+
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
30+
import org.springframework.web.servlet.DispatcherServlet;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
/**
35+
* Tests for {@link SecurityRequestMatcherAutoConfiguration}.
36+
*
37+
* @author Wang Zhiyang
38+
*/
39+
public class SecurityRequestMatcherAutoConfigurationTests {
40+
41+
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
42+
.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class, SecurityAutoConfiguration.class,
43+
PropertyPlaceholderAutoConfiguration.class, SecurityMvcRequestMatcherConfiguration.class));
44+
45+
@Test
46+
void testSecurityMvcRequestMatcherConfiguration() {
47+
this.contextRunner.withUserConfiguration(TestExactlyOneDispatcherServletConfig.class)
48+
.withPropertyValues("spring.mvc.servlet.path=/custom")
49+
.run((context) -> {
50+
MvcRequestMatcher.Builder builder = context.getBean(MvcRequestMatcher.Builder.class);
51+
assertThat(builder).isNotNull();
52+
MvcRequestMatcher mvcRequestMatcher = builder.pattern("/example");
53+
assertThat(mvcRequestMatcher).extracting("servletPath").isEqualTo("/custom");
54+
assertThat(mvcRequestMatcher).extracting("pattern").isEqualTo("/example");
55+
});
56+
}
57+
58+
@Test
59+
void testMultipleDispatcherServletMvcRequestMatcherConfiguration() {
60+
this.contextRunner.withUserConfiguration(TestMultipleDispatcherServletConfig.class)
61+
.run((context) -> assertThat(context.containsBean("mvcRequestMatcherBuilder")).isFalse());
62+
}
63+
64+
@Test
65+
void testPrototypeDispatcherServletMvcRequestMatcherConfiguration() {
66+
this.contextRunner.withUserConfiguration(TestPrototypeDispatcherServletConfig.class)
67+
.run((context) -> assertThat(context.containsBean("mvcRequestMatcherBuilder")).isFalse());
68+
}
69+
70+
@Test
71+
void testNoDispatcherServletMvcRequestMatcherConfiguration() {
72+
this.contextRunner.run((context) -> assertThat(context.containsBean("mvcRequestMatcherBuilder")).isFalse());
73+
}
74+
75+
@Configuration
76+
static class TestExactlyOneDispatcherServletConfig {
77+
78+
@Bean
79+
DispatcherServlet dispatcherServlet() {
80+
return new DispatcherServlet();
81+
}
82+
83+
}
84+
85+
@Configuration
86+
static class TestMultipleDispatcherServletConfig {
87+
88+
@Bean
89+
DispatcherServlet dispatcherServlet() {
90+
return new DispatcherServlet();
91+
}
92+
93+
@Bean("dispatcherServletTwo")
94+
DispatcherServlet dispatcherServletTwo() {
95+
return new DispatcherServlet();
96+
}
97+
98+
}
99+
100+
@Configuration
101+
static class TestPrototypeDispatcherServletConfig {
102+
103+
@Bean
104+
@Scope("prototype")
105+
DispatcherServlet dispatcherServlet() {
106+
return new DispatcherServlet();
107+
}
108+
109+
}
110+
111+
}

0 commit comments

Comments
 (0)