diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java index c01b030a81..51755db999 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfiguration.java @@ -53,6 +53,55 @@ public class CorsConfiguration { public CorsConfiguration() { } + /** + * Copy constructor. + */ + public CorsConfiguration(CorsConfiguration other) { + this.allowedOrigins = other.allowedOrigins; + this.allowedMethods = other.allowedMethods; + this.allowedHeaders = other.allowedHeaders; + this.exposedHeaders = other.exposedHeaders; + this.allowCredentials = other.allowCredentials; + this.maxAge = other.maxAge; + } + + /** + * Combine the specified {@link CorsConfiguration} with this one. + * Properties of this configuration are overridden only by non-null properties + * of the provided one. + * @return the combined {@link CorsConfiguration} + */ + public CorsConfiguration combine(CorsConfiguration other) { + if (other == null) { + return this; + } + CorsConfiguration config = new CorsConfiguration(this); + config.setAllowedOrigins(combine(this.getAllowedOrigins(), other.getAllowedOrigins())); + config.setAllowedMethods(combine(this.getAllowedMethods(), other.getAllowedMethods())); + config.setAllowedHeaders(combine(this.getAllowedHeaders(), other.getAllowedHeaders())); + config.setExposedHeaders(combine(this.getExposedHeaders(), other.getExposedHeaders())); + Boolean allowCredentials = other.getAllowCredentials(); + if (allowCredentials != null) { + config.setAllowCredentials(allowCredentials); + } + Long maxAge = other.getMaxAge(); + if (maxAge != null) { + config.setMaxAge(maxAge); + } + return config; + } + + private List combine(List source, List other) { + if (other == null) { + return source; + } + if (source == null || source.contains("*")) { + return other; + } + List combined = new ArrayList(source); + combined.addAll(other); + return combined; + } /** * Configure origins to allow, e.g. "http://domain1.com". The special value @@ -142,6 +191,9 @@ public class CorsConfiguration { *

By default this is not set. */ public void setExposedHeaders(List exposedHeaders) { + if (exposedHeaders != null && exposedHeaders.contains("*")) { + throw new IllegalArgumentException("'*' is not a valid exposed header value"); + } this.exposedHeaders = exposedHeaders; } @@ -149,6 +201,9 @@ public class CorsConfiguration { * Add a single response header to expose. */ public void addExposedHeader(String exposedHeader) { + if ("*".equals(exposedHeader)) { + throw new IllegalArgumentException("'*' is not a valid exposed header value"); + } if (this.exposedHeaders == null) { this.exposedHeaders = new ArrayList(); } diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java index 081290c9e0..1881ddde9f 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationTests.java @@ -20,8 +20,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; @@ -40,6 +39,115 @@ public class CorsConfigurationTests { public void setup() { config = new CorsConfiguration(); } + + @Test + public void setNullValues() { + config.setAllowedOrigins(null); + assertNull(config.getAllowedOrigins()); + config.setAllowedHeaders(null); + assertNull(config.getAllowedHeaders()); + config.setAllowedMethods(null); + assertNull(config.getAllowedMethods()); + config.setExposedHeaders(null); + assertNull(config.getExposedHeaders()); + config.setAllowCredentials(null); + assertNull(config.getAllowCredentials()); + config.setMaxAge(null); + assertNull(config.getMaxAge()); + } + + @Test + public void setValues() { + config.addAllowedOrigin("*"); + assertEquals(Arrays.asList("*"), config.getAllowedOrigins()); + config.addAllowedHeader("*"); + assertEquals(Arrays.asList("*"), config.getAllowedHeaders()); + config.addAllowedMethod("*"); + assertEquals(Arrays.asList("*"), config.getAllowedMethods()); + config.addExposedHeader("header1"); + config.addExposedHeader("header2"); + assertEquals(Arrays.asList("header1", "header2"), config.getExposedHeaders()); + config.setAllowCredentials(true); + assertTrue(config.getAllowCredentials()); + config.setMaxAge(123L); + assertEquals(new Long(123), config.getMaxAge()); + } + + @Test(expected = IllegalArgumentException.class) + public void asteriskWildCardOnAddExposedHeader() { + config.addExposedHeader("*"); + } + + @Test(expected = IllegalArgumentException.class) + public void asteriskWildCardOnSetExposedHeaders() { + config.setExposedHeaders(Arrays.asList("*")); + } + + @Test + public void combineWithNull() { + config.setAllowedOrigins(Arrays.asList("*")); + config.combine(null); + assertEquals(Arrays.asList("*"), config.getAllowedOrigins()); + } + + @Test + public void combineWithNullProperties() { + config.addAllowedOrigin("*"); + config.addAllowedHeader("header1"); + config.addExposedHeader("header3"); + config.addAllowedMethod(HttpMethod.GET.name()); + config.setMaxAge(123L); + config.setAllowCredentials(true); + CorsConfiguration other = new CorsConfiguration(); + config = config.combine(other); + assertEquals(Arrays.asList("*"), config.getAllowedOrigins()); + assertEquals(Arrays.asList("header1"), config.getAllowedHeaders()); + assertEquals(Arrays.asList("header3"), config.getExposedHeaders()); + assertEquals(Arrays.asList(HttpMethod.GET.name()), config.getAllowedMethods()); + assertEquals(new Long(123), config.getMaxAge()); + assertTrue(config.getAllowCredentials()); + } + + @Test + public void combineWithAsteriskWildCard() { + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + CorsConfiguration other = new CorsConfiguration(); + other.addAllowedOrigin("http://domain.com"); + other.addAllowedHeader("header1"); + other.addExposedHeader("header2"); + other.addAllowedMethod(HttpMethod.PUT.name()); + config = config.combine(other); + assertEquals(Arrays.asList("http://domain.com"), config.getAllowedOrigins()); + assertEquals(Arrays.asList("header1"), config.getAllowedHeaders()); + assertEquals(Arrays.asList("header2"), config.getExposedHeaders()); + assertEquals(Arrays.asList(HttpMethod.PUT.name()), config.getAllowedMethods()); + } + + @Test + public void combine() { + config.addAllowedOrigin("http://domain1.com"); + config.addAllowedHeader("header1"); + config.addExposedHeader("header3"); + config.addAllowedMethod(HttpMethod.GET.name()); + config.setMaxAge(123L); + config.setAllowCredentials(true); + CorsConfiguration other = new CorsConfiguration(); + other.addAllowedOrigin("http://domain2.com"); + other.addAllowedHeader("header2"); + other.addExposedHeader("header4"); + other.addAllowedMethod(HttpMethod.PUT.name()); + other.setMaxAge(456L); + other.setAllowCredentials(false); + config = config.combine(other); + assertEquals(Arrays.asList("http://domain1.com", "http://domain2.com"), config.getAllowedOrigins()); + assertEquals(Arrays.asList("header1", "header2"), config.getAllowedHeaders()); + assertEquals(Arrays.asList("header3", "header4"), config.getExposedHeaders()); + assertEquals(Arrays.asList(HttpMethod.GET.name(), HttpMethod.PUT.name()), config.getAllowedMethods()); + assertEquals(new Long(456), config.getMaxAge()); + assertFalse(config.getAllowCredentials()); + } @Test public void checkOriginAllowed() { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CrossOriginConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CrossOriginConfigurer.java new file mode 100644 index 0000000000..bd62567018 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CrossOriginConfigurer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config.annotation; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.web.cors.CorsConfiguration; + +/** + * Assist with the registration of {@link CorsConfiguration} mapped to one or more path patterns. + * @author Sebastien Deleuze + * + * @since 4.2 + * @see CrossOriginRegistration + */ +public class CrossOriginConfigurer { + + private final List registrations = new ArrayList(); + + /** + * Enable cross origin requests on the specified path patterns. If no path pattern is specified, + * cross-origin request handling is mapped on "/**" . + * + *

By default, all origins, all headers and credentials are allowed. Max age is set to 30 minutes.

+ */ + public CrossOriginRegistration enableCrossOrigin(String... pathPatterns) { + CrossOriginRegistration registration = new CrossOriginRegistration(pathPatterns); + this.registrations.add(registration); + return registration; + } + + protected Map getCorsConfigurations() { + Map configs = new HashMap(); + for (CrossOriginRegistration registration : this.registrations) { + for (String pathPattern : registration.getPathPatterns()) { + configs.put(pathPattern, registration.getCorsConfiguration()); + } + } + return configs; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CrossOriginRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CrossOriginRegistration.java new file mode 100644 index 0000000000..2af6900931 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CrossOriginRegistration.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config.annotation; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.springframework.http.HttpMethod; +import org.springframework.web.cors.CorsConfiguration; + +/** + * Assists with the creation of a {@link CorsConfiguration} mapped to one or more path patterns. + * If no path pattern is specified, cross-origin request handling is mapped on "/**" . + * + *

By default, all origins, all headers, credentials and GET, HEAD, POST methods are allowed. + * Max age is set to 30 minutes.

+ * + * @author Sebastien Deleuze + * @since 4.2 + */ +public class CrossOriginRegistration { + + private final String[] pathPatterns; + + private final CorsConfiguration config; + + public CrossOriginRegistration(String... pathPatterns) { + this.pathPatterns = (pathPatterns.length == 0 ? new String[]{ "/**" } : pathPatterns); + // Same default values than @CrossOrigin annotation + allows simple methods + this.config = new CorsConfiguration(); + this.config.addAllowedOrigin("*"); + this.config.addAllowedMethod(HttpMethod.GET.name()); + this.config.addAllowedMethod(HttpMethod.HEAD.name()); + this.config.addAllowedMethod(HttpMethod.POST.name()); + this.config.addAllowedHeader("*"); + this.config.setAllowCredentials(true); + this.config.setMaxAge(1800L); + } + + public CrossOriginRegistration allowedOrigins(String... origins) { + this.config.setAllowedOrigins(new ArrayList(Arrays.asList(origins))); + return this; + } + + public CrossOriginRegistration allowedMethods(String... methods) { + this.config.setAllowedMethods(new ArrayList(Arrays.asList(methods))); + return this; + } + + public CrossOriginRegistration allowedHeaders(String... headers) { + this.config.setAllowedHeaders(new ArrayList(Arrays.asList(headers))); + return this; + } + + public CrossOriginRegistration exposedHeaders(String... headers) { + this.config.setExposedHeaders(new ArrayList(Arrays.asList(headers))); + return this; + } + + public CrossOriginRegistration maxAge(long maxAge) { + this.config.setMaxAge(maxAge); + return this; + } + + public CrossOriginRegistration allowCredentials(boolean allowCredentials) { + this.config.setAllowCredentials(allowCredentials); + return this; + } + + protected String[] getPathPatterns() { + return this.pathPatterns; + } + + protected CorsConfiguration getCorsConfiguration() { + return this.config; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java index bf384ce8ea..ea0724daff 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java @@ -132,4 +132,9 @@ public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { this.configurers.configureHandlerExceptionResolvers(exceptionResolvers); } + @Override + protected void configureCrossOrigin(CrossOriginConfigurer configurer) { + this.configurers.configureCrossOrigin(configurer); + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index fe63763026..b3f57793cf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -66,6 +66,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.ServletContextAware; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.support.CompositeUriComponentsContributor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -199,6 +200,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private ContentNegotiationManager contentNegotiationManager; private List> messageConverters; + + private Map corsConfigurations; /** @@ -236,6 +239,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager()); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); PathMatchConfigurer configurer = getPathMatchConfigurer(); if (configurer.isUseSuffixPatternMatch() != null) { @@ -367,6 +371,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setPathMatcher(mvcPathMatcher()); handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); handlerMapping.setInterceptors(getInterceptors()); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); return handlerMapping; } @@ -386,6 +391,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping(); mapping.setOrder(2); mapping.setInterceptors(getInterceptors()); + mapping.setCorsConfigurations(getCorsConfigurations()); return mapping; } @@ -405,6 +411,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); handlerMapping.setInterceptors(new HandlerInterceptor[] { new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider())}); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); } else { handlerMapping = new EmptyHandlerMapping(); @@ -863,6 +870,26 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv protected void configureViewResolvers(ViewResolverRegistry registry) { } + /** + * @since 4.2 + */ + protected final Map getCorsConfigurations() { + if (this.corsConfigurations == null) { + CrossOriginConfigurer registry = new CrossOriginConfigurer(); + configureCrossOrigin(registry); + this.corsConfigurations = registry.getCorsConfigurations(); + } + return this.corsConfigurations; + } + + /** + * Override this method to configure cross-origin requests handling. + * @since 4.2 + * @see CrossOriginConfigurer + */ + protected void configureCrossOrigin(CrossOriginConfigurer configurer) { + } + private static final class EmptyHandlerMapping extends AbstractHandlerMapping { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java index 85c09d382b..a9a40502ec 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java @@ -182,4 +182,10 @@ public interface WebMvcConfigurer { */ void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer); + /** + * Configure cross-origin requests handling. + * @since 4.2 + */ + void configureCrossOrigin(CrossOriginConfigurer configurer); + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java index 7f4c7c34a8..46a79fb1a5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java @@ -165,4 +165,12 @@ public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer { public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { } + /** + * {@inheritDoc} + *

This implementation is empty. + */ + @Override + public void configureCrossOrigin(CrossOriginConfigurer configurer) { + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java index af08decd7f..ed5c65e753 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java @@ -153,6 +153,13 @@ class WebMvcConfigurerComposite implements WebMvcConfigurer { return selectSingleInstance(candidates, Validator.class); } + @Override + public void configureCrossOrigin(CrossOriginConfigurer configurer) { + for (WebMvcConfigurer delegate : this.delegates) { + delegate.configureCrossOrigin(configurer); + } + } + private T selectSingleInstance(List instances, Class instanceType) { if (instances.size() > 1) { throw new IllegalStateException( 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 367dcb765b..b97ebf78e2 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 @@ -19,7 +19,9 @@ package org.springframework.web.servlet.handler; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -80,6 +82,8 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport private final List mappedInterceptors = new ArrayList(); private CorsProcessor corsProcessor = new DefaultCorsProcessor(); + + private Map corsConfigurations = new HashMap(); /** @@ -201,6 +205,28 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport Assert.notNull(corsProcessor, "CorsProcessor must not be null"); this.corsProcessor = corsProcessor; } + + /** + * Map the specified {@link CorsConfiguration} to the specified path. + * + * @param pathPattern the path to use.

Supports direct URL matches and Ant-style pattern matches. + * For syntax details, see the {@link org.springframework.util.AntPathMatcher} javadoc. + * @param config the CORS configuration to use + * @since 4.2 + */ + public void registerCorsConfiguration(String pathPattern, CorsConfiguration config) { + this.corsConfigurations.put(pathPattern, config); + } + + /** + * Set the {@link CorsConfiguration} map. + * + * @since 4.2 + * @see #registerCorsConfiguration(String, CorsConfiguration) + */ + public void setCorsConfigurations(Map corsConfigurations) { + this.corsConfigurations = corsConfigurations; + } /** * Initializes the interceptors. @@ -327,8 +353,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (CorsUtils.isCorsRequest(request)) { - CorsConfiguration config = getCorsConfiguration(handler, request); - executionChain = getCorsHandlerExecutionChain(request, executionChain, config); + executionChain = getCorsHandlerExecutionChain(request, executionChain); } return executionChain; } @@ -415,7 +440,17 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport * HandlerInterceptor that makes CORS-related checks and adds CORS headers. */ protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, - HandlerExecutionChain chain, CorsConfiguration config) { + HandlerExecutionChain chain) { + + CorsConfiguration globalConfig = null; + String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); + for(Map.Entry entry : this.corsConfigurations.entrySet()) { + if(this.pathMatcher.match(entry.getKey(), lookupPath)) { + globalConfig = entry.getValue(); + } + } + CorsConfiguration config = getCorsConfiguration(chain.getHandler(), request); + config = (globalConfig == null ? config : globalConfig.combine(config)); if (config != null) { if (CorsUtils.isPreFlightRequest(request)) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index 2acb0f83ff..59d98d81a8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -436,17 +436,18 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap @Override protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) { + CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request); if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; - CorsConfiguration corsConfig = this.mappingRegistry.getCorsConfiguration(handlerMethod); - if (corsConfig != null) { - return corsConfig; - } - else if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) { + if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) { return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG; } + else { + CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod); + corsConfig = (corsConfig == null ? corsConfigFromMethod : corsConfig.combine(corsConfigFromMethod)); + } } - return null; + return corsConfig; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CrossOriginConfigurerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CrossOriginConfigurerTests.java new file mode 100644 index 0000000000..09f91f57d0 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/CrossOriginConfigurerTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2015 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config.annotation; + +import java.util.Arrays; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.web.cors.CorsConfiguration; + +/** + * Test fixture with a {@link CrossOriginConfigurer}. + * + * @author Sebastien Deleuze + */ +public class CrossOriginConfigurerTests { + + private CrossOriginConfigurer configurer; + + @Before + public void setUp() { + this.configurer = new CrossOriginConfigurer(); + } + + @Test + public void noCrossOriginConfigured() { + assertTrue(this.configurer.getCorsConfigurations().isEmpty()); + } + + @Test + public void multipleCrossOriginConfigured() { + this.configurer.enableCrossOrigin("/foo"); + this.configurer.enableCrossOrigin("/bar"); + assertEquals(2, this.configurer.getCorsConfigurations().size()); + } + + @Test + public void defaultCrossOriginRegistration() { + this.configurer.enableCrossOrigin(); + Map configs = this.configurer.getCorsConfigurations(); + assertEquals(1, configs.size()); + CorsConfiguration config = configs.get("/**"); + assertEquals(Arrays.asList("*"), config.getAllowedOrigins()); + assertEquals(Arrays.asList("GET", "HEAD", "POST"), config.getAllowedMethods()); + assertEquals(Arrays.asList("*"), config.getAllowedHeaders()); + assertEquals(true, config.getAllowCredentials()); + assertEquals(Long.valueOf(1800), config.getMaxAge()); + } + + @Test + public void customizedCrossOriginRegistration() { + this.configurer.enableCrossOrigin("/foo").allowedOrigins("http://domain2.com", "http://domain2.com") + .allowedMethods("DELETE").allowCredentials(false).allowedHeaders("header1", "header2") + .exposedHeaders("header3", "header4").maxAge(3600); + Map configs = this.configurer.getCorsConfigurations(); + assertEquals(1, configs.size()); + CorsConfiguration config = configs.get("/foo"); + assertEquals(Arrays.asList("http://domain2.com", "http://domain2.com"), config.getAllowedOrigins()); + assertEquals(Arrays.asList("DELETE"), config.getAllowedMethods()); + assertEquals(Arrays.asList("header1", "header2"), config.getAllowedHeaders()); + assertEquals(Arrays.asList("header3", "header4"), config.getExposedHeaders()); + assertEquals(false, config.getAllowCredentials()); + assertEquals(Long.valueOf(3600), config.getMaxAge()); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java index bc93d4ef7d..76868d3752 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportExtensionTests.java @@ -18,6 +18,7 @@ package org.springframework.web.servlet.config.annotation; import java.util.Arrays; import java.util.List; +import java.util.Map; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; @@ -56,6 +57,7 @@ import org.springframework.web.context.request.async.CallableProcessingIntercept import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; import org.springframework.web.context.request.async.DeferredResultProcessingInterceptorAdapter; import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -269,6 +271,13 @@ public class WebMvcConfigurationSupportExtensionTests { assertEquals("/", accessor.getPropertyValue("prefix")); assertEquals(".jsp", accessor.getPropertyValue("suffix")); } + + @Test + public void crossOrigin() { + Map configs = this.config.getCorsConfigurations(); + assertEquals(1, configs.size()); + assertEquals("*", configs.get("/resources/**").getAllowedOrigins().get(0)); + } @Controller @@ -393,6 +402,12 @@ public class WebMvcConfigurationSupportExtensionTests { public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable("default"); } + + @Override + public void configureCrossOrigin(CrossOriginConfigurer registry) { + registry.enableCrossOrigin("/resources/**"); + } + } private class TestPathHelper extends UrlPathHelper {} 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 615ae81e9d..2ed5086fd6 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 @@ -63,7 +63,7 @@ public class CorsAbstractHandlerMappingTests { @Test public void actualRequestWithoutCorsConfigurationProvider() throws Exception { this.request.setMethod(RequestMethod.GET.name()); - this.request.setRequestURI("/notcors"); + this.request.setRequestURI("/foo"); this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com/test.html"); this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); HandlerExecutionChain chain = handlerMapping.getHandler(this.request); @@ -73,7 +73,7 @@ public class CorsAbstractHandlerMappingTests { @Test public void preflightRequestWithoutCorsConfigurationProvider() throws Exception { this.request.setMethod(RequestMethod.OPTIONS.name()); - this.request.setRequestURI("/notcors"); + this.request.setRequestURI("/foo"); this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com/test.html"); this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); HandlerExecutionChain chain = handlerMapping.getHandler(this.request); @@ -106,6 +106,39 @@ public class CorsAbstractHandlerMappingTests { assertNotNull(config); assertArrayEquals(config.getAllowedOrigins().toArray(), new String[]{"*"}); } + + @Test + public void actualRequestWithMappedCorsConfiguration() throws Exception { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("*"); + this.handlerMapping.registerCorsConfiguration("/foo", config); + this.request.setMethod(RequestMethod.GET.name()); + this.request.setRequestURI("/foo"); + this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com/test.html"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + HandlerExecutionChain chain = handlerMapping.getHandler(this.request); + assertTrue(chain.getHandler() instanceof SimpleHandler); + config = getCorsConfiguration(chain, false); + assertNotNull(config); + assertArrayEquals(config.getAllowedOrigins().toArray(), new String[]{"*"}); + } + + @Test + public void preflightRequestWithMappedCorsConfiguration() throws Exception { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("*"); + this.handlerMapping.registerCorsConfiguration("/foo", config); + this.request.setMethod(RequestMethod.OPTIONS.name()); + this.request.setRequestURI("/foo"); + this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com/test.html"); + this.request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + HandlerExecutionChain chain = handlerMapping.getHandler(this.request); + assertNotNull(chain.getHandler()); + assertTrue(chain.getHandler().getClass().getSimpleName().equals("PreFlightHandler")); + config = getCorsConfiguration(chain, true); + assertNotNull(config); + assertArrayEquals(config.getAllowedOrigins().toArray(), new String[]{"*"}); + } private CorsConfiguration getCorsConfiguration(HandlerExecutionChain chain, boolean isPreFlightRequest) {