Skip to content

Commit 7e9b710

Browse files
committed
Support custom CorsConfigurationSource in AbstractHandlerMapping
This commit allows to specify a custom CorsConfigurationSource in AbstractHandlerMapping (both Servlet and Reactive variants). AbstractHandlerMapping#getCorsConfigurations method is now deprecated. Issue: SPR-17067
1 parent b325c74 commit 7e9b710

File tree

5 files changed

+167
-26
lines changed

5 files changed

+167
-26
lines changed

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

+20-7
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
5353

5454
private final PathPatternParser patternParser;
5555

56-
private final UrlBasedCorsConfigurationSource globalCorsConfigSource;
56+
private CorsConfigurationSource corsConfigurationSource;
5757

5858
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
5959

@@ -65,7 +65,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport
6565

6666
public AbstractHandlerMapping() {
6767
this.patternParser = new PathPatternParser();
68-
this.globalCorsConfigSource = new UrlBasedCorsConfigurationSource(this.patternParser);
68+
this.corsConfigurationSource = new UrlBasedCorsConfigurationSource(this.patternParser);
6969
}
7070

7171

@@ -107,12 +107,25 @@ public PathPatternParser getPathPatternParser() {
107107
}
108108

109109
/**
110-
* Set "global" CORS configuration based on URL patterns. By default the
111-
* first matching URL pattern is combined with handler-level CORS
112-
* configuration if any.
110+
* Set the "global" CORS configurations based on URL patterns. By default the
111+
* first matching URL pattern is combined with handler-level CORS configuration if any.
112+
* @see #setCorsConfigurationSource(CorsConfigurationSource)
113113
*/
114114
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
115-
this.globalCorsConfigSource.setCorsConfigurations(corsConfigurations);
115+
Assert.notNull(corsConfigurations, "corsConfigurations must not be null");
116+
this.corsConfigurationSource = new UrlBasedCorsConfigurationSource(this.patternParser);
117+
((UrlBasedCorsConfigurationSource) this.corsConfigurationSource).setCorsConfigurations(corsConfigurations);
118+
}
119+
120+
/**
121+
* Set the "global" CORS configuration source. By default the first matching URL
122+
* pattern is combined with the CORS configuration for the handler, if any.
123+
* @since 5.1
124+
* @see #setCorsConfigurations(Map)
125+
*/
126+
public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) {
127+
Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null");
128+
this.corsConfigurationSource = corsConfigurationSource;
116129
}
117130

118131
/**
@@ -163,7 +176,7 @@ public Mono<Object> getHandler(ServerWebExchange exchange) {
163176
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
164177
}
165178
if (CorsUtils.isCorsRequest(exchange.getRequest())) {
166-
CorsConfiguration configA = this.globalCorsConfigSource.getCorsConfiguration(exchange);
179+
CorsConfiguration configA = this.corsConfigurationSource.getCorsConfiguration(exchange);
167180
CorsConfiguration configB = getCorsConfiguration(handler, exchange);
168181
CorsConfiguration config = (configA != null ? configA.combine(configB) : configB);
169182
if (!getCorsProcessor().process(config, exchange) ||

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

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -130,6 +130,38 @@ public void preFlightRequestWithGlobalCorsConfig() throws Exception {
130130
assertEquals("*", exchange.getResponse().getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
131131
}
132132

133+
@Test
134+
public void actualRequestWithCorsConfigurationSource() throws Exception {
135+
this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource());
136+
137+
String origin = "http://domain2.com";
138+
ServerWebExchange exchange = createExchange(HttpMethod.GET, "/welcome.html", origin);
139+
Object actual = this.handlerMapping.getHandler(exchange).block();
140+
141+
assertNotNull(actual);
142+
assertSame(this.welcomeController, actual);
143+
assertEquals("http://domain2.com", exchange.getResponse().getHeaders()
144+
.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
145+
assertEquals("true", exchange.getResponse().getHeaders()
146+
.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS));
147+
}
148+
149+
@Test
150+
public void preFlightRequestWithCorsConfigurationSource() throws Exception {
151+
this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource());
152+
153+
String origin = "http://domain2.com";
154+
ServerWebExchange exchange = createExchange(HttpMethod.OPTIONS, "/welcome.html", origin);
155+
Object actual = this.handlerMapping.getHandler(exchange).block();
156+
157+
assertNotNull(actual);
158+
assertNotSame(this.welcomeController, actual);
159+
assertEquals("http://domain2.com", exchange.getResponse().getHeaders()
160+
.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
161+
assertEquals("true", exchange.getResponse().getHeaders()
162+
.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS));
163+
}
164+
133165

134166
private ServerWebExchange createExchange(HttpMethod method, String path, String origin) {
135167

@@ -150,4 +182,15 @@ public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
150182
}
151183
}
152184

185+
public class CustomCorsConfigurationSource implements CorsConfigurationSource {
186+
187+
@Override
188+
public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) {
189+
CorsConfiguration config = new CorsConfiguration();
190+
config.addAllowedOrigin("*");
191+
config.setAllowCredentials(true);
192+
return config;
193+
}
194+
}
195+
153196
}

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

+51-15
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
8181

8282
private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<>();
8383

84-
private final UrlBasedCorsConfigurationSource globalCorsConfigSource = new UrlBasedCorsConfigurationSource();
84+
private CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
8585

8686
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
8787

@@ -115,7 +115,9 @@ public Object getDefaultHandler() {
115115
*/
116116
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
117117
this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
118-
this.globalCorsConfigSource.setAlwaysUseFullPath(alwaysUseFullPath);
118+
if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) {
119+
((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setAlwaysUseFullPath(alwaysUseFullPath);
120+
}
119121
}
120122

121123
/**
@@ -124,7 +126,9 @@ public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
124126
*/
125127
public void setUrlDecode(boolean urlDecode) {
126128
this.urlPathHelper.setUrlDecode(urlDecode);
127-
this.globalCorsConfigSource.setUrlDecode(urlDecode);
129+
if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) {
130+
((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setUrlDecode(urlDecode);
131+
}
128132
}
129133

130134
/**
@@ -133,7 +137,9 @@ public void setUrlDecode(boolean urlDecode) {
133137
*/
134138
public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
135139
this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
136-
this.globalCorsConfigSource.setRemoveSemicolonContent(removeSemicolonContent);
140+
if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) {
141+
((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setRemoveSemicolonContent(removeSemicolonContent);
142+
}
137143
}
138144

139145
/**
@@ -145,7 +151,9 @@ public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
145151
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
146152
Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
147153
this.urlPathHelper = urlPathHelper;
148-
this.globalCorsConfigSource.setUrlPathHelper(urlPathHelper);
154+
if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) {
155+
((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setUrlPathHelper(urlPathHelper);
156+
}
149157
}
150158

151159
/**
@@ -163,7 +171,9 @@ public UrlPathHelper getUrlPathHelper() {
163171
public void setPathMatcher(PathMatcher pathMatcher) {
164172
Assert.notNull(pathMatcher, "PathMatcher must not be null");
165173
this.pathMatcher = pathMatcher;
166-
this.globalCorsConfigSource.setPathMatcher(pathMatcher);
174+
if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) {
175+
((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setPathMatcher(pathMatcher);
176+
}
167177
}
168178

169179
/**
@@ -189,20 +199,45 @@ public void setInterceptors(Object... interceptors) {
189199
}
190200

191201
/**
192-
* Set "global" CORS configuration based on URL patterns. By default the first
193-
* matching URL pattern is combined with the CORS configuration for the
194-
* handler, if any.
202+
* Set the "global" CORS configurations based on URL patterns. By default the first
203+
* matching URL pattern is combined with the CORS configuration for the handler, if any.
195204
* @since 4.2
205+
* @see #setCorsConfigurationSource(CorsConfigurationSource)
196206
*/
197207
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
198-
this.globalCorsConfigSource.setCorsConfigurations(corsConfigurations);
208+
Assert.notNull(corsConfigurations, "corsConfigurations must not be null");
209+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
210+
source.setCorsConfigurations(corsConfigurations);
211+
source.setPathMatcher(this.pathMatcher);
212+
source.setUrlPathHelper(this.urlPathHelper);
213+
this.corsConfigurationSource = source;
214+
}
215+
216+
/**
217+
* Set the "global" CORS configuration source. By default the first matching URL
218+
* pattern is combined with the CORS configuration for the handler, if any.
219+
* @since 5.1
220+
* @see #setCorsConfigurations(Map)
221+
*/
222+
public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) {
223+
Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null");
224+
this.corsConfigurationSource = corsConfigurationSource;
199225
}
200226

201227
/**
202-
* Get the "global" CORS configuration.
228+
* Get the "global" CORS configurations.
229+
* @deprecated as of 5.1 since it is now possible to set a {@link CorsConfigurationSource} which is not a
230+
* {@link UrlBasedCorsConfigurationSource}. Expected to be removed in 5.2.
203231
*/
232+
@Deprecated
204233
public Map<String, CorsConfiguration> getCorsConfigurations() {
205-
return this.globalCorsConfigSource.getCorsConfigurations();
234+
if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) {
235+
return ((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).getCorsConfigurations();
236+
}
237+
else {
238+
throw new IllegalStateException("No CORS configurations available when the source " +
239+
"is not an UrlBasedCorsConfigurationSource");
240+
}
206241
}
207242

208243
/**
@@ -286,7 +321,8 @@ protected void detectMappedInterceptors(List<HandlerInterceptor> mappedIntercept
286321

287322
/**
288323
* Initialize the specified interceptors, checking for {@link MappedInterceptor MappedInterceptors} and
289-
* adapting {@link HandlerInterceptor}s and {@link WebRequestInterceptor HandlerInterceptor}s and {@link WebRequestInterceptors} if necessary.
324+
* adapting {@link HandlerInterceptor}s and {@link WebRequestInterceptor HandlerInterceptor}s and
325+
* {@link WebRequestInterceptor}s if necessary.
290326
* @see #setInterceptors
291327
* @see #adaptInterceptor
292328
*/
@@ -385,7 +421,7 @@ else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(Dispatch
385421
}
386422

387423
if (CorsUtils.isCorsRequest(request)) {
388-
CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
424+
CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
389425
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
390426
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
391427
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
@@ -402,7 +438,7 @@ else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(Dispatch
402438
* the pre-flight request but for the expected actual request based on the URL
403439
* path, the HTTP methods from the "Access-Control-Request-Method" header, and
404440
* the headers from the "Access-Control-Request-Headers" header thus allowing
405-
* the CORS configuration to be obtained via {@link #getCorsConfigurations},
441+
* the CORS configuration to be obtained via {@link #getCorsConfiguration(Object, HttpServletRequest)},
406442
* <p>Note: This method may also return a pre-built {@link HandlerExecutionChain},
407443
* combining a handler object with dynamically determined interceptors.
408444
* Statically specified interceptors will get merged into such an existing chain.

spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor;
8686
import org.springframework.web.context.support.XmlWebApplicationContext;
8787
import org.springframework.web.cors.CorsConfiguration;
88+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
8889
import org.springframework.web.method.HandlerMethod;
8990
import org.springframework.web.method.support.CompositeUriComponentsContributor;
9091
import org.springframework.web.method.support.InvocableHandlerMethod;
@@ -887,7 +888,9 @@ public void testCorsMinimal() throws Exception {
887888
for (String beanName : beanNames) {
888889
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName);
889890
assertNotNull(handlerMapping);
890-
Map<String, CorsConfiguration> configs = handlerMapping.getCorsConfigurations();
891+
DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping);
892+
Map<String, CorsConfiguration> configs = ((UrlBasedCorsConfigurationSource)accessor
893+
.getPropertyValue("corsConfigurationSource")).getCorsConfigurations();
891894
assertNotNull(configs);
892895
assertEquals(1, configs.size());
893896
CorsConfiguration config = configs.get("/**");
@@ -910,7 +913,9 @@ public void testCors() throws Exception {
910913
for (String beanName : beanNames) {
911914
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName);
912915
assertNotNull(handlerMapping);
913-
Map<String, CorsConfiguration> configs = handlerMapping.getCorsConfigurations();
916+
DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping);
917+
Map<String, CorsConfiguration> configs = ((UrlBasedCorsConfigurationSource)accessor
918+
.getPropertyValue("corsConfigurationSource")).getCorsConfigurations();
914919
assertNotNull(configs);
915920
assertEquals(2, configs.size());
916921
CorsConfiguration config = configs.get("/api/**");

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

+45-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -149,6 +149,39 @@ public void preflightRequestWithMappedCorsConfiguration() throws Exception {
149149
assertArrayEquals(config.getAllowedOrigins().toArray(), new String[]{"*"});
150150
}
151151

152+
@Test
153+
public void actualRequestWithCorsConfigurationSource() throws Exception {
154+
this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource());
155+
this.request.setMethod(RequestMethod.GET.name());
156+
this.request.setRequestURI("/foo");
157+
this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com");
158+
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
159+
HandlerExecutionChain chain = handlerMapping.getHandler(this.request);
160+
assertNotNull(chain);
161+
assertTrue(chain.getHandler() instanceof SimpleHandler);
162+
CorsConfiguration config = getCorsConfiguration(chain, false);
163+
assertNotNull(config);
164+
assertArrayEquals(new String[]{"*"}, config.getAllowedOrigins().toArray());
165+
assertEquals(true, config.getAllowCredentials());
166+
}
167+
168+
@Test
169+
public void preflightRequestWithCorsConfigurationSource() throws Exception {
170+
this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource());
171+
this.request.setMethod(RequestMethod.OPTIONS.name());
172+
this.request.setRequestURI("/foo");
173+
this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com");
174+
this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET");
175+
HandlerExecutionChain chain = handlerMapping.getHandler(this.request);
176+
assertNotNull(chain);
177+
assertNotNull(chain.getHandler());
178+
assertTrue(chain.getHandler().getClass().getSimpleName().equals("PreFlightHandler"));
179+
CorsConfiguration config = getCorsConfiguration(chain, true);
180+
assertNotNull(config);
181+
assertArrayEquals(new String[]{"*"}, config.getAllowedOrigins().toArray());
182+
assertEquals(true, config.getAllowCredentials());
183+
}
184+
152185

153186
private CorsConfiguration getCorsConfiguration(HandlerExecutionChain chain, boolean isPreFlightRequest) {
154187
if (isPreFlightRequest) {
@@ -208,4 +241,15 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
208241

209242
}
210243

244+
public class CustomCorsConfigurationSource implements CorsConfigurationSource {
245+
246+
@Override
247+
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
248+
CorsConfiguration config = new CorsConfiguration();
249+
config.addAllowedOrigin("*");
250+
config.setAllowCredentials(true);
251+
return config;
252+
}
253+
}
254+
211255
}

0 commit comments

Comments
 (0)