Skip to content

Commit bc7d010

Browse files
committed
Update CORS support
This commit updates CORS support in order to check Origin header in CorsUtils#isPreFlightRequest which does not change how Spring MVC or WebFlux process CORS request but is more correct in term of behavior since it is a public API potentially used in another contexts. It also removes an unnecessary check in AbstractHandlerMethodMapping#hasCorsConfigurationSource and processes every preflight request with PreFlightHandler. Closes gh-24327
1 parent 8396e6b commit bc7d010

File tree

10 files changed

+74
-26
lines changed

10 files changed

+74
-26
lines changed

spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -66,12 +66,12 @@ else if ("https".equals(scheme) || "wss".equals(scheme)) {
6666
}
6767

6868
/**
69-
* Returns {@code true} if the request is a valid CORS pre-flight one.
70-
* To be used in combination with {@link #isCorsRequest(HttpServletRequest)} since
71-
* regular CORS checks are not invoked here for performance reasons.
69+
* Returns {@code true} if the request is a valid CORS pre-flight one by checking {code OPTIONS} method with
70+
* {@code Origin} and {@code Access-Control-Request-Method} headers presence.
7271
*/
7372
public static boolean isPreFlightRequest(HttpServletRequest request) {
7473
return (HttpMethod.OPTIONS.matches(request.getMethod()) &&
74+
request.getHeader(HttpHeaders.ORIGIN) != null &&
7575
request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
7676
}
7777

spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -45,12 +45,14 @@ public static boolean isCorsRequest(ServerHttpRequest request) {
4545
}
4646

4747
/**
48-
* Returns {@code true} if the request is a valid CORS pre-flight one.
49-
* To be used in combination with {@link #isCorsRequest(ServerHttpRequest)} since
50-
* regular CORS checks are not invoked here for performance reasons.
48+
* Returns {@code true} if the request is a valid CORS pre-flight one by checking {code OPTIONS} method with
49+
* {@code Origin} and {@code Access-Control-Request-Method} headers presence.
5150
*/
5251
public static boolean isPreFlightRequest(ServerHttpRequest request) {
53-
return (request.getMethod() == HttpMethod.OPTIONS && request.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
52+
HttpHeaders headers = request.getHeaders();
53+
return (request.getMethod() == HttpMethod.OPTIONS
54+
&& headers.containsKey(HttpHeaders.ORIGIN)
55+
&& headers.containsKey(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
5456
}
5557

5658
/**

spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -182,8 +182,8 @@ public Mono<Object> getHandler(ServerWebExchange exchange) {
182182
if (logger.isDebugEnabled()) {
183183
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
184184
}
185-
if (hasCorsConfigurationSource(handler)) {
186-
ServerHttpRequest request = exchange.getRequest();
185+
ServerHttpRequest request = exchange.getRequest();
186+
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
187187
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
188188
CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
189189
config = (config != null ? config.combine(handlerConfig) : handlerConfig);

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -374,8 +374,7 @@ protected HandlerMethod handleNoMatch(Set<T> mappings, ServerWebExchange exchang
374374
@Override
375375
protected boolean hasCorsConfigurationSource(Object handler) {
376376
return super.hasCorsConfigurationSource(handler) ||
377-
(handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null) ||
378-
handler.equals(PREFLIGHT_AMBIGUOUS_MATCH);
377+
(handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null);
379378
}
380379

381380
@Override

spring-webflux/src/test/java/org/springframework/web/reactive/handler/CorsUrlHandlerMappingTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -70,7 +70,7 @@ public void preflightRequestWithoutCorsConfigurationProvider() throws Exception
7070
Object actual = this.handlerMapping.getHandler(exchange).block();
7171

7272
assertThat(actual).isNotNull();
73-
assertThat(actual).isSameAs(this.welcomeController);
73+
assertThat(actual).isNotSameAs(this.welcomeController);
7474
}
7575

7676
@Test

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/CrossOriginAnnotationIntegrationTests.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -35,11 +35,13 @@
3535
import org.springframework.web.bind.annotation.RequestMapping;
3636
import org.springframework.web.bind.annotation.RequestMethod;
3737
import org.springframework.web.bind.annotation.RestController;
38+
import org.springframework.web.client.HttpClientErrorException;
3839
import org.springframework.web.client.RestTemplate;
3940
import org.springframework.web.reactive.config.EnableWebFlux;
4041
import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer;
4142

4243
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.assertj.core.api.Assertions.fail;
4345

4446
/**
4547
* Integration tests with {@code @CrossOrigin} and {@code @RequestMapping}
@@ -89,6 +91,28 @@ void actualGetRequestWithoutAnnotation(HttpServer httpServer) throws Exception {
8991
assertThat(entity.getBody()).isEqualTo("no");
9092
}
9193

94+
@ParameterizedHttpServerTest
95+
void optionsRequestWithAccessControlRequestMethod(HttpServer httpServer) throws Exception {
96+
startServer(httpServer);
97+
this.headers.clear();
98+
this.headers.add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
99+
ResponseEntity<String> entity = performOptions("/no", this.headers, String.class);
100+
assertThat(entity.getBody()).isNull();
101+
}
102+
103+
@ParameterizedHttpServerTest
104+
void preflightRequestWithoutAnnotation(HttpServer httpServer) throws Exception {
105+
startServer(httpServer);
106+
this.headers.add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
107+
try {
108+
performOptions("/no", this.headers, Void.class);
109+
fail("Preflight request without CORS configuration should fail");
110+
}
111+
catch (HttpClientErrorException ex) {
112+
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
113+
}
114+
}
115+
92116
@ParameterizedHttpServerTest
93117
void actualPostRequestWithoutAnnotation(HttpServer httpServer) throws Exception {
94118
startServer(httpServer);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -414,7 +414,7 @@ else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(Dispatch
414414
logger.debug("Mapped to " + executionChain.getHandler());
415415
}
416416

417-
if (hasCorsConfigurationSource(handler)) {
417+
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
418418
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
419419
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
420420
config = (config != null ? config.combine(handlerConfig) : handlerConfig);

spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -458,8 +458,7 @@ protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpSe
458458
@Override
459459
protected boolean hasCorsConfigurationSource(Object handler) {
460460
return super.hasCorsConfigurationSource(handler) ||
461-
(handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null) ||
462-
handler.equals(PREFLIGHT_AMBIGUOUS_MATCH);
461+
(handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null);
463462
}
464463

465464
@Override

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -86,7 +86,8 @@ void preflightRequestWithoutCorsConfigurationProvider() throws Exception {
8686
HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request);
8787

8888
assertThat(chain).isNotNull();
89-
assertThat(chain.getHandler()).isInstanceOf(SimpleHandler.class);
89+
assertThat(chain.getHandler()).isNotNull();
90+
assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler");
9091
}
9192

9293
@Test

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -69,6 +69,10 @@ public class CrossOriginTests {
6969

7070
private final MockHttpServletRequest request = new MockHttpServletRequest();
7171

72+
private final String optionsHandler = "org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler#handle()";
73+
74+
private final String corsPreflightHandler = "org.springframework.web.servlet.handler.AbstractHandlerMapping$PreFlightHandler";
75+
7276

7377
@BeforeEach
7478
@SuppressWarnings("resource")
@@ -96,6 +100,25 @@ public void noAnnotationWithoutOrigin() throws Exception {
96100
assertThat(getCorsConfiguration(chain, false)).isNull();
97101
}
98102

103+
@Test
104+
public void noAnnotationWithAccessControlRequestMethod() throws Exception {
105+
this.handlerMapping.registerHandler(new MethodLevelController());
106+
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/no");
107+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
108+
HandlerExecutionChain chain = this.handlerMapping.getHandler(request);
109+
assertThat(chain.getHandler().toString()).isEqualTo(optionsHandler);
110+
}
111+
112+
@Test
113+
public void noAnnotationWithPreflightRequest() throws Exception {
114+
this.handlerMapping.registerHandler(new MethodLevelController());
115+
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/no");
116+
request.addHeader(HttpHeaders.ORIGIN, "https://domain.com/");
117+
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
118+
HandlerExecutionChain chain = this.handlerMapping.getHandler(request);
119+
assertThat(chain.getHandler().getClass().getName()).isEqualTo(corsPreflightHandler);
120+
}
121+
99122
@Test // SPR-12931
100123
public void noAnnotationWithOrigin() throws Exception {
101124
this.handlerMapping.registerHandler(new MethodLevelController());

0 commit comments

Comments
 (0)