Skip to content

Commit 75a5409

Browse files
committed
Add PreFlightRequestHandler for Spring MVC
This is equivalent of the same contract for WebFlux. It is implemented by HandlerMappingIntrospector, and may be called directly by Spring Security to handle a pre-flight request without delegate to the rest of the filter chain. HandlerMappingIntrospector also has the boolean method allHandlerMappingsUsePathPatternParser that checks whether all handler mappings are configured to use parsed PathPattern's. See gh-31823
1 parent 3991cae commit 75a5409

File tree

6 files changed

+182
-18
lines changed

6 files changed

+182
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2002-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.web.cors;
18+
19+
import jakarta.servlet.http.HttpServletRequest;
20+
import jakarta.servlet.http.HttpServletResponse;
21+
22+
/**
23+
* Handler for CORS pre-flight requests.
24+
*
25+
* @author Rossen Stoyanchev
26+
* @since 6.2
27+
*/
28+
public interface PreFlightRequestHandler {
29+
30+
/**
31+
* Handle a pre-flight request by finding and applying the CORS configuration
32+
* that matches the expected actual request. As a result of handling, the
33+
* response should be updated with CORS headers or rejected with
34+
* {@link org.springframework.http.HttpStatus#FORBIDDEN}.
35+
* @param request current HTTP request
36+
* @param response current HTTP response
37+
*/
38+
void handlePreFlight(HttpServletRequest request, HttpServletResponse response) throws Exception;
39+
40+
}

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java

+24-10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.web.servlet.handler;
1818

19+
import java.io.IOException;
1920
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.List;
@@ -47,6 +48,7 @@
4748
import org.springframework.web.cors.CorsProcessor;
4849
import org.springframework.web.cors.CorsUtils;
4950
import org.springframework.web.cors.DefaultCorsProcessor;
51+
import org.springframework.web.cors.PreFlightRequestHandler;
5052
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
5153
import org.springframework.web.servlet.DispatcherServlet;
5254
import org.springframework.web.servlet.HandlerExecutionChain;
@@ -679,9 +681,9 @@ protected HandlerExecutionChain getCorsHandlerExecutionChain(
679681
HttpServletRequest request, HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
680682

681683
if (CorsUtils.isPreFlightRequest(request)) {
682-
PreFlightHandler preFlightHandler = new PreFlightHandler(config);
683-
chain.addInterceptor(0, preFlightHandler);
684-
return new HandlerExecutionChain(preFlightHandler, chain.getInterceptors());
684+
PreFlightHttpRequestHandler handler = new PreFlightHttpRequestHandler(config);
685+
chain.addInterceptor(0, handler);
686+
return new HandlerExecutionChain(handler, chain.getInterceptors());
685687
}
686688
else {
687689
chain.addInterceptor(0, new CorsInterceptor(config));
@@ -699,6 +701,12 @@ public CorsInterceptor(@Nullable CorsConfiguration config) {
699701
this.config = config;
700702
}
701703

704+
@Override
705+
@Nullable
706+
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
707+
return this.config;
708+
}
709+
702710
@Override
703711
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
704712
throws Exception {
@@ -709,27 +717,33 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
709717
return true;
710718
}
711719

712-
return corsProcessor.processRequest(this.config, request, response);
720+
return invokeCorsProcessor(request, response);
713721
}
714722

715-
@Override
716-
@Nullable
717-
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
718-
return this.config;
723+
protected boolean invokeCorsProcessor(
724+
HttpServletRequest request, HttpServletResponse response) throws IOException {
725+
726+
return corsProcessor.processRequest(this.config, request, response);
719727
}
720728
}
721729

722730

723-
private class PreFlightHandler extends CorsInterceptor implements HttpRequestHandler {
731+
private final class PreFlightHttpRequestHandler
732+
extends CorsInterceptor implements HttpRequestHandler, PreFlightRequestHandler {
724733

725-
public PreFlightHandler(@Nullable CorsConfiguration config) {
734+
public PreFlightHttpRequestHandler(@Nullable CorsConfiguration config) {
726735
super(config);
727736
}
728737

729738
@Override
730739
public void handleRequest(HttpServletRequest request, HttpServletResponse response) {
731740
// no-op
732741
}
742+
743+
@Override
744+
public void handlePreFlight(HttpServletRequest request, HttpServletResponse response) throws IOException {
745+
invokeCorsProcessor(request, response);
746+
}
733747
}
734748

735749
}

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java

+49-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import jakarta.servlet.ServletRequest;
3434
import jakarta.servlet.http.HttpServletRequest;
3535
import jakarta.servlet.http.HttpServletRequestWrapper;
36+
import jakarta.servlet.http.HttpServletResponse;
3637
import org.apache.commons.logging.Log;
3738
import org.apache.commons.logging.LogFactory;
3839

@@ -45,16 +46,20 @@
4546
import org.springframework.core.io.Resource;
4647
import org.springframework.core.io.support.PropertiesLoaderUtils;
4748
import org.springframework.http.server.RequestPath;
49+
import org.springframework.http.server.ServletServerHttpRequest;
4850
import org.springframework.lang.Nullable;
4951
import org.springframework.util.Assert;
5052
import org.springframework.util.ClassUtils;
5153
import org.springframework.util.StringUtils;
5254
import org.springframework.web.cors.CorsConfiguration;
5355
import org.springframework.web.cors.CorsConfigurationSource;
56+
import org.springframework.web.cors.CorsUtils;
57+
import org.springframework.web.cors.PreFlightRequestHandler;
5458
import org.springframework.web.servlet.DispatcherServlet;
5559
import org.springframework.web.servlet.HandlerExecutionChain;
5660
import org.springframework.web.servlet.HandlerInterceptor;
5761
import org.springframework.web.servlet.HandlerMapping;
62+
import org.springframework.web.servlet.NoHandlerFoundException;
5863
import org.springframework.web.util.ServletRequestPathUtils;
5964
import org.springframework.web.util.UrlPathHelper;
6065
import org.springframework.web.util.pattern.PathPatternParser;
@@ -87,7 +92,7 @@
8792
* @since 4.3.1
8893
*/
8994
public class HandlerMappingIntrospector
90-
implements CorsConfigurationSource, ApplicationContextAware, InitializingBean {
95+
implements CorsConfigurationSource, PreFlightRequestHandler, ApplicationContextAware, InitializingBean {
9196

9297
private static final Log logger = LogFactory.getLog(HandlerMappingIntrospector.class.getName());
9398

@@ -172,6 +177,49 @@ public List<HandlerMapping> getHandlerMappings() {
172177
return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList());
173178
}
174179

180+
/**
181+
* Return {@code true} if all {@link HandlerMapping} beans
182+
* {@link HandlerMapping#usesPathPatterns() use parsed PathPatterns},
183+
* and {@code false} if any don't.
184+
* @since 6.2
185+
*/
186+
public boolean allHandlerMappingsUsePathPatternParser() {
187+
Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet.");
188+
return getHandlerMappings().stream().allMatch(HandlerMapping::usesPathPatterns);
189+
}
190+
191+
192+
/**
193+
* Find the matching {@link HandlerMapping} for the request, and invoke the
194+
* handler it returns as a {@link PreFlightRequestHandler}.
195+
* @throws NoHandlerFoundException if no handler matches the request
196+
* @since 6.2
197+
*/
198+
public void handlePreFlight(HttpServletRequest request, HttpServletResponse response) throws Exception {
199+
Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet.");
200+
Assert.state(CorsUtils.isPreFlightRequest(request), "Not a pre-flight request.");
201+
RequestPath previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
202+
try {
203+
ServletRequestPathUtils.parseAndCache(request);
204+
for (HandlerMapping mapping : this.handlerMappings) {
205+
HandlerExecutionChain chain = mapping.getHandler(request);
206+
if (chain != null) {
207+
Object handler = chain.getHandler();
208+
if (handler instanceof PreFlightRequestHandler preFlightHandler) {
209+
preFlightHandler.handlePreFlight(request, response);
210+
return;
211+
}
212+
throw new IllegalStateException("Expected PreFlightRequestHandler: " + handler.getClass());
213+
}
214+
}
215+
throw new NoHandlerFoundException(
216+
request.getMethod(), request.getRequestURI(), new ServletServerHttpRequest(request).getHeaders());
217+
}
218+
finally {
219+
ServletRequestPathUtils.setParsedRequestPath(previousPath, request);
220+
}
221+
}
222+
175223

176224
/**
177225
* {@link Filter} that looks up the {@code MatchableHandlerMapping} and

Diff for: spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.web.context.support.StaticWebApplicationContext;
3232
import org.springframework.web.cors.CorsConfiguration;
3333
import org.springframework.web.cors.CorsConfigurationSource;
34+
import org.springframework.web.cors.PreFlightRequestHandler;
3435
import org.springframework.web.servlet.HandlerExecutionChain;
3536
import org.springframework.web.servlet.HandlerInterceptor;
3637
import org.springframework.web.servlet.support.WebContentGenerator;
@@ -72,7 +73,7 @@ void preflightRequestWithoutCorsConfig(TestHandlerMapping mapping) throws Except
7273

7374
assertThat(chain).isNotNull();
7475
assertThat(chain.getHandler()).isNotNull();
75-
assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler");
76+
assertThat(chain.getHandler()).isInstanceOf(PreFlightRequestHandler.class);
7677
assertThat(mapping.hasSavedCorsConfig()).isFalse();
7778
}
7879

@@ -103,7 +104,7 @@ void preflightRequestWithCorsConfigProvider(TestHandlerMapping mapping) throws E
103104

104105
assertThat(chain).isNotNull();
105106
assertThat(chain.getHandler()).isNotNull();
106-
assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler");
107+
assertThat(chain.getHandler()).isInstanceOf(PreFlightRequestHandler.class);
107108
assertThat(mapping.getRequiredCorsConfig().getAllowedOrigins()).containsExactly("*");
108109
}
109110

@@ -144,7 +145,7 @@ void preflightRequestWithMappedCorsConfig(TestHandlerMapping mapping) throws Exc
144145

145146
assertThat(chain).isNotNull();
146147
assertThat(chain.getHandler()).isNotNull();
147-
assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler");
148+
assertThat(chain.getHandler()).isInstanceOf(PreFlightRequestHandler.class);
148149
assertThat(mapping.getRequiredCorsConfig().getAllowedOrigins()).containsExactly("*");
149150
}
150151

@@ -172,7 +173,7 @@ void preflightRequestWithCorsConfigSource(TestHandlerMapping mapping) throws Exc
172173

173174
assertThat(chain).isNotNull();
174175
assertThat(chain.getHandler()).isNotNull();
175-
assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler");
176+
assertThat(chain.getHandler()).isInstanceOf(PreFlightRequestHandler.class);
176177

177178
CorsConfiguration config = mapping.getRequiredCorsConfig();
178179
assertThat(config).isNotNull();

Diff for: spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java

+60
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.web.cors.CorsConfigurationSource;
4949
import org.springframework.web.servlet.HandlerExecutionChain;
5050
import org.springframework.web.servlet.HandlerMapping;
51+
import org.springframework.web.servlet.NoHandlerFoundException;
5152
import org.springframework.web.servlet.function.RouterFunction;
5253
import org.springframework.web.servlet.function.RouterFunctions;
5354
import org.springframework.web.servlet.function.ServerResponse;
@@ -63,6 +64,7 @@
6364

6465
import static org.assertj.core.api.Assertions.assertThat;
6566
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
67+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
6668
import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE;
6769

6870
/**
@@ -113,6 +115,29 @@ void detectHandlerMappingsOrdered() {
113115
assertThat(actual).isEqualTo(expected);
114116
}
115117

118+
@Test
119+
void useParsedPatternsOnly() {
120+
GenericWebApplicationContext context = new GenericWebApplicationContext();
121+
context.registerBean("A", SimpleUrlHandlerMapping.class);
122+
context.registerBean("B", SimpleUrlHandlerMapping.class);
123+
context.registerBean("C", SimpleUrlHandlerMapping.class);
124+
context.refresh();
125+
126+
assertThat(initIntrospector(context).allHandlerMappingsUsePathPatternParser()).isTrue();
127+
128+
context = new GenericWebApplicationContext();
129+
context.registerBean("A", SimpleUrlHandlerMapping.class);
130+
context.registerBean("B", SimpleUrlHandlerMapping.class);
131+
context.registerBean("C", SimpleUrlHandlerMapping.class, () -> {
132+
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
133+
mapping.setPatternParser(null);
134+
return mapping;
135+
});
136+
context.refresh();
137+
138+
assertThat(initIntrospector(context).allHandlerMappingsUsePathPatternParser()).isFalse();
139+
}
140+
116141
@ParameterizedTest
117142
@ValueSource(booleans = {true, false})
118143
void getMatchable(boolean usePathPatterns) throws Exception {
@@ -204,6 +229,41 @@ void getCorsConfigurationActual() {
204229
assertThat(corsConfig.getAllowedMethods()).isEqualTo(Collections.singletonList("POST"));
205230
}
206231

232+
@Test
233+
void handlePreFlight() throws Exception {
234+
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
235+
context.register(TestConfig.class);
236+
context.refresh();
237+
238+
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path");
239+
request.addHeader("Origin", "http://localhost:9000");
240+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");
241+
MockHttpServletResponse response = new MockHttpServletResponse();
242+
243+
initIntrospector(context).handlePreFlight(request, response);
244+
245+
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("http://localhost:9000");
246+
assertThat(response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).containsExactly("POST");
247+
}
248+
249+
@Test
250+
void handlePreFlightWithNoHandlerFoundException() {
251+
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
252+
context.register(TestConfig.class);
253+
context.refresh();
254+
255+
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/unknownPath");
256+
request.addHeader("Origin", "http://localhost:9000");
257+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");
258+
MockHttpServletResponse response = new MockHttpServletResponse();
259+
260+
assertThatThrownBy(() -> initIntrospector(context).handlePreFlight(request, response))
261+
.isInstanceOf(NoHandlerFoundException.class);
262+
263+
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isNull();
264+
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isNull();
265+
}
266+
207267
@ParameterizedTest
208268
@ValueSource(strings = {"/test", "/resource/1234****"}) // gh-31937
209269
void cacheFilter(String uri) throws Exception {

Diff for: spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -47,6 +47,7 @@
4747
import org.springframework.web.bind.annotation.RequestMethod;
4848
import org.springframework.web.context.support.StaticWebApplicationContext;
4949
import org.springframework.web.cors.CorsConfiguration;
50+
import org.springframework.web.cors.PreFlightRequestHandler;
5051
import org.springframework.web.servlet.HandlerExecutionChain;
5152
import org.springframework.web.servlet.HandlerInterceptor;
5253
import org.springframework.web.servlet.handler.PathPatternsParameterizedTest;
@@ -127,7 +128,7 @@ void noAnnotationWithPreflightRequest(TestRequestMappingInfoHandlerMapping mappi
127128
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
128129
HandlerExecutionChain chain = mapping.getHandler(request);
129130
assertThat(chain).isNotNull();
130-
assertThat(chain.getHandler().getClass().getName()).endsWith("AbstractHandlerMapping$PreFlightHandler");
131+
assertThat(chain.getHandler()).isInstanceOf(PreFlightRequestHandler.class);
131132
}
132133

133134
@PathPatternsParameterizedTest // SPR-12931
@@ -389,7 +390,7 @@ private CorsConfiguration getCorsConfiguration(@Nullable HandlerExecutionChain c
389390
assertThat(chain).isNotNull();
390391
if (isPreFlightRequest) {
391392
Object handler = chain.getHandler();
392-
assertThat(handler.getClass().getSimpleName()).isEqualTo("PreFlightHandler");
393+
assertThat(handler).isInstanceOf(PreFlightRequestHandler.class);
393394
DirectFieldAccessor accessor = new DirectFieldAccessor(handler);
394395
return (CorsConfiguration)accessor.getPropertyValue("config");
395396
}

0 commit comments

Comments
 (0)