diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java index c877a8b935..05937ba806 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -53,7 +53,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport private final PathPatternParser patternParser; - private final UrlBasedCorsConfigurationSource globalCorsConfigSource; + private CorsConfigurationSource corsConfigurationSource; private CorsProcessor corsProcessor = new DefaultCorsProcessor(); @@ -65,7 +65,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport public AbstractHandlerMapping() { this.patternParser = new PathPatternParser(); - this.globalCorsConfigSource = new UrlBasedCorsConfigurationSource(this.patternParser); + this.corsConfigurationSource = new UrlBasedCorsConfigurationSource(this.patternParser); } @@ -107,12 +107,25 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport } /** - * Set "global" CORS configuration based on URL patterns. By default the - * first matching URL pattern is combined with handler-level CORS - * configuration if any. + * Set the "global" CORS configurations based on URL patterns. By default the + * first matching URL pattern is combined with handler-level CORS configuration if any. + * @see #setCorsConfigurationSource(CorsConfigurationSource) */ public void setCorsConfigurations(Map corsConfigurations) { - this.globalCorsConfigSource.setCorsConfigurations(corsConfigurations); + Assert.notNull(corsConfigurations, "corsConfigurations must not be null"); + this.corsConfigurationSource = new UrlBasedCorsConfigurationSource(this.patternParser); + ((UrlBasedCorsConfigurationSource) this.corsConfigurationSource).setCorsConfigurations(corsConfigurations); + } + + /** + * Set the "global" CORS configuration source. By default the first matching URL + * pattern is combined with the CORS configuration for the handler, if any. + * @since 5.1 + * @see #setCorsConfigurations(Map) + */ + public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) { + Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null"); + this.corsConfigurationSource = corsConfigurationSource; } /** @@ -163,7 +176,7 @@ public abstract class AbstractHandlerMapping extends ApplicationObjectSupport logger.debug(exchange.getLogPrefix() + "Mapped to " + handler); } if (CorsUtils.isCorsRequest(exchange.getRequest())) { - CorsConfiguration configA = this.globalCorsConfigSource.getCorsConfiguration(exchange); + CorsConfiguration configA = this.corsConfigurationSource.getCorsConfiguration(exchange); CorsConfiguration configB = getCorsConfiguration(handler, exchange); CorsConfiguration config = (configA != null ? configA.combine(configB) : configB); if (!getCorsProcessor().process(config, exchange) || diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/handler/CorsUrlHandlerMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/handler/CorsUrlHandlerMappingTests.java index dfba9e5c0c..60fec12d4f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/handler/CorsUrlHandlerMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/handler/CorsUrlHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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. @@ -130,6 +130,38 @@ public class CorsUrlHandlerMappingTests { assertEquals("*", exchange.getResponse().getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); } + @Test + public void actualRequestWithCorsConfigurationSource() throws Exception { + this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource()); + + String origin = "http://domain2.com"; + ServerWebExchange exchange = createExchange(HttpMethod.GET, "/welcome.html", origin); + Object actual = this.handlerMapping.getHandler(exchange).block(); + + assertNotNull(actual); + assertSame(this.welcomeController, actual); + assertEquals("http://domain2.com", exchange.getResponse().getHeaders() + .getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertEquals("true", exchange.getResponse().getHeaders() + .getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + + @Test + public void preFlightRequestWithCorsConfigurationSource() throws Exception { + this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource()); + + String origin = "http://domain2.com"; + ServerWebExchange exchange = createExchange(HttpMethod.OPTIONS, "/welcome.html", origin); + Object actual = this.handlerMapping.getHandler(exchange).block(); + + assertNotNull(actual); + assertNotSame(this.welcomeController, actual); + assertEquals("http://domain2.com", exchange.getResponse().getHeaders() + .getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertEquals("true", exchange.getResponse().getHeaders() + .getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + private ServerWebExchange createExchange(HttpMethod method, String path, String origin) { @@ -150,4 +182,15 @@ public class CorsUrlHandlerMappingTests { } } + public class CustomCorsConfigurationSource implements CorsConfigurationSource { + + @Override + public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("*"); + config.setAllowCredentials(true); + return config; + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index 77984ca17f..0948e799da 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -81,7 +81,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport private final List adaptedInterceptors = new ArrayList<>(); - private final UrlBasedCorsConfigurationSource globalCorsConfigSource = new UrlBasedCorsConfigurationSource(); + private CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource(); private CorsProcessor corsProcessor = new DefaultCorsProcessor(); @@ -115,7 +115,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); - this.globalCorsConfigSource.setAlwaysUseFullPath(alwaysUseFullPath); + if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { + ((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setAlwaysUseFullPath(alwaysUseFullPath); + } } /** @@ -124,7 +126,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setUrlDecode(boolean urlDecode) { this.urlPathHelper.setUrlDecode(urlDecode); - this.globalCorsConfigSource.setUrlDecode(urlDecode); + if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { + ((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setUrlDecode(urlDecode); + } } /** @@ -133,7 +137,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setRemoveSemicolonContent(boolean removeSemicolonContent) { this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); - this.globalCorsConfigSource.setRemoveSemicolonContent(removeSemicolonContent); + if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { + ((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setRemoveSemicolonContent(removeSemicolonContent); + } } /** @@ -145,7 +151,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport public void setUrlPathHelper(UrlPathHelper urlPathHelper) { Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); this.urlPathHelper = urlPathHelper; - this.globalCorsConfigSource.setUrlPathHelper(urlPathHelper); + if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { + ((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setUrlPathHelper(urlPathHelper); + } } /** @@ -163,7 +171,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport public void setPathMatcher(PathMatcher pathMatcher) { Assert.notNull(pathMatcher, "PathMatcher must not be null"); this.pathMatcher = pathMatcher; - this.globalCorsConfigSource.setPathMatcher(pathMatcher); + if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { + ((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).setPathMatcher(pathMatcher); + } } /** @@ -189,20 +199,45 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Set "global" CORS configuration based on URL patterns. By default the first - * matching URL pattern is combined with the CORS configuration for the - * handler, if any. + * Set the "global" CORS configurations based on URL patterns. By default the first + * matching URL pattern is combined with the CORS configuration for the handler, if any. * @since 4.2 + * @see #setCorsConfigurationSource(CorsConfigurationSource) */ public void setCorsConfigurations(Map corsConfigurations) { - this.globalCorsConfigSource.setCorsConfigurations(corsConfigurations); + Assert.notNull(corsConfigurations, "corsConfigurations must not be null"); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.setCorsConfigurations(corsConfigurations); + source.setPathMatcher(this.pathMatcher); + source.setUrlPathHelper(this.urlPathHelper); + this.corsConfigurationSource = source; } /** - * Get the "global" CORS configuration. + * Set the "global" CORS configuration source. By default the first matching URL + * pattern is combined with the CORS configuration for the handler, if any. + * @since 5.1 + * @see #setCorsConfigurations(Map) */ + public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) { + Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null"); + this.corsConfigurationSource = corsConfigurationSource; + } + + /** + * Get the "global" CORS configurations. + * @deprecated as of 5.1 since it is now possible to set a {@link CorsConfigurationSource} which is not a + * {@link UrlBasedCorsConfigurationSource}. Expected to be removed in 5.2. + */ + @Deprecated public Map getCorsConfigurations() { - return this.globalCorsConfigSource.getCorsConfigurations(); + if (this.corsConfigurationSource instanceof UrlBasedCorsConfigurationSource) { + return ((UrlBasedCorsConfigurationSource)this.corsConfigurationSource).getCorsConfigurations(); + } + else { + throw new IllegalStateException("No CORS configurations available when the source " + + "is not an UrlBasedCorsConfigurationSource"); + } } /** @@ -286,7 +321,8 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport /** * Initialize the specified interceptors, checking for {@link MappedInterceptor MappedInterceptors} and - * adapting {@link HandlerInterceptor}s and {@link WebRequestInterceptor HandlerInterceptor}s and {@link WebRequestInterceptors} if necessary. + * adapting {@link HandlerInterceptor}s and {@link WebRequestInterceptor HandlerInterceptor}s and + * {@link WebRequestInterceptor}s if necessary. * @see #setInterceptors * @see #adaptInterceptor */ @@ -385,7 +421,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } if (CorsUtils.isCorsRequest(request)) { - CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request); + CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); @@ -402,7 +438,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport * the pre-flight request but for the expected actual request based on the URL * path, the HTTP methods from the "Access-Control-Request-Method" header, and * the headers from the "Access-Control-Request-Headers" header thus allowing - * the CORS configuration to be obtained via {@link #getCorsConfigurations}, + * the CORS configuration to be obtained via {@link #getCorsConfiguration(Object, HttpServletRequest)}, *

Note: This method may also return a pre-built {@link HandlerExecutionChain}, * combining a handler object with dynamically determined interceptors. * Statically specified interceptors will get merged into such an existing chain. diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index cf3b2a5200..19b7dda662 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -85,6 +85,7 @@ import org.springframework.web.context.request.async.CallableProcessingIntercept import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; import org.springframework.web.context.support.XmlWebApplicationContext; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.CompositeUriComponentsContributor; import org.springframework.web.method.support.InvocableHandlerMethod; @@ -887,7 +888,9 @@ public class MvcNamespaceTests { for (String beanName : beanNames) { AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName); assertNotNull(handlerMapping); - Map configs = handlerMapping.getCorsConfigurations(); + DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping); + Map configs = ((UrlBasedCorsConfigurationSource)accessor + .getPropertyValue("corsConfigurationSource")).getCorsConfigurations(); assertNotNull(configs); assertEquals(1, configs.size()); CorsConfiguration config = configs.get("/**"); @@ -910,7 +913,9 @@ public class MvcNamespaceTests { for (String beanName : beanNames) { AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName); assertNotNull(handlerMapping); - Map configs = handlerMapping.getCorsConfigurations(); + DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping); + Map configs = ((UrlBasedCorsConfigurationSource)accessor + .getPropertyValue("corsConfigurationSource")).getCorsConfigurations(); assertNotNull(configs); assertEquals(2, configs.size()); CorsConfiguration config = configs.get("/api/**"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java index 1a91186a98..b76f5922f8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2018 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. @@ -149,6 +149,39 @@ public class CorsAbstractHandlerMappingTests { assertArrayEquals(config.getAllowedOrigins().toArray(), new String[]{"*"}); } + @Test + public void actualRequestWithCorsConfigurationSource() throws Exception { + this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource()); + this.request.setMethod(RequestMethod.GET.name()); + this.request.setRequestURI("/foo"); + this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + HandlerExecutionChain chain = handlerMapping.getHandler(this.request); + assertNotNull(chain); + assertTrue(chain.getHandler() instanceof SimpleHandler); + CorsConfiguration config = getCorsConfiguration(chain, false); + assertNotNull(config); + assertArrayEquals(new String[]{"*"}, config.getAllowedOrigins().toArray()); + assertEquals(true, config.getAllowCredentials()); + } + + @Test + public void preflightRequestWithCorsConfigurationSource() throws Exception { + this.handlerMapping.setCorsConfigurationSource(new CustomCorsConfigurationSource()); + this.request.setMethod(RequestMethod.OPTIONS.name()); + this.request.setRequestURI("/foo"); + this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + HandlerExecutionChain chain = handlerMapping.getHandler(this.request); + assertNotNull(chain); + assertNotNull(chain.getHandler()); + assertTrue(chain.getHandler().getClass().getSimpleName().equals("PreFlightHandler")); + CorsConfiguration config = getCorsConfiguration(chain, true); + assertNotNull(config); + assertArrayEquals(new String[]{"*"}, config.getAllowedOrigins().toArray()); + assertEquals(true, config.getAllowCredentials()); + } + private CorsConfiguration getCorsConfiguration(HandlerExecutionChain chain, boolean isPreFlightRequest) { if (isPreFlightRequest) { @@ -208,4 +241,15 @@ public class CorsAbstractHandlerMappingTests { } + public class CustomCorsConfigurationSource implements CorsConfigurationSource { + + @Override + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("*"); + config.setAllowCredentials(true); + return config; + } + } + }