diff --git a/spring-web/src/main/java/org/springframework/web/cors/CorsConfigurationMapping.java b/spring-web/src/main/java/org/springframework/web/cors/CorsConfigurationMapping.java new file mode 100644 index 0000000000..8fd146d643 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/cors/CorsConfigurationMapping.java @@ -0,0 +1,134 @@ +/* + * 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.cors; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; +import org.springframework.web.util.UrlPathHelper; + +/** + * Provide a per request {@link CorsConfiguration} instance based on a collection of + * {@link CorsConfiguration} mapped on path patterns. + * + *

Exact path mapping URIs (such as {@code "/admin"}) are supported as + * well as Ant-style path patterns (such as {@code "/admin/**"}). + * + * @author Sebastien Deleuze + * @since 4.2 + */ +public class CorsConfigurationMapping implements CorsConfigurationSource { + + private final Map corsConfigurations = + new LinkedHashMap(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + private UrlPathHelper urlPathHelper = new UrlPathHelper(); + + + /** + * Set the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + } + + /** + * Set if URL lookup should always use the full path within the current servlet + * context. Else, the path within the current servlet mapping is used if applicable + * (that is, in the case of a ".../*" servlet mapping in web.xml). + *

Default is "false". + * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath + */ + public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { + this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + } + + /** + * Set if context path and request URI should be URL-decoded. Both are returned + * undecoded by the Servlet API, in contrast to the servlet path. + *

Uses either the request encoding or the default encoding according + * to the Servlet spec (ISO-8859-1). + * @see org.springframework.web.util.UrlPathHelper#setUrlDecode + */ + public void setUrlDecode(boolean urlDecode) { + this.urlPathHelper.setUrlDecode(urlDecode); + } + + /** + * Set if ";" (semicolon) content should be stripped from the request URI. + *

The default value is {@code true}. + * @see org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean) + */ + public void setRemoveSemicolonContent(boolean removeSemicolonContent) { + this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); + } + + /** + * Set the UrlPathHelper to use for resolution of lookup paths. + *

Use this to override the default UrlPathHelper with a custom subclass. + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); + this.urlPathHelper = urlPathHelper; + } + + /** + * Set CORS configuration based on URL patterns. + */ + public void setCorsConfigurations(Map corsConfigurations) { + this.corsConfigurations.clear(); + if (corsConfigurations != null) { + this.corsConfigurations.putAll(corsConfigurations); + } + } + + /** + * Get the CORS configuration. + */ + public Map getCorsConfigurations() { + return Collections.unmodifiableMap(this.corsConfigurations); + } + + /** + * Register a {@link CorsConfiguration} for the specified path pattern. + */ + public void registerCorsConfiguration(String path, CorsConfiguration config) { + this.corsConfigurations.put(path, config); + } + + @Override + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); + for(Map.Entry entry : this.corsConfigurations.entrySet()) { + if (this.pathMatcher.match(entry.getKey(), lookupPath)) { + return entry.getValue(); + } + } + return null; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java b/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java new file mode 100644 index 0000000000..613f15e9ac --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/filter/CorsFilter.java @@ -0,0 +1,89 @@ +/* + * 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.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.Assert; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationMapping; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.CorsProcessor; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.DefaultCorsProcessor; + +/** + * {@link javax.servlet.Filter} that handles CORS preflight requests and intercepts CORS + * simple and actual requests thanks to a {@link CorsProcessor} implementation + * ({@link DefaultCorsProcessor} by default) in order to add the relevant CORS response + * headers (like {@code Access-Control-Allow-Origin}) using the provided + * {@link CorsConfigurationSource} (for example a {@link CorsConfigurationMapping} instance. + * + *

This filter could be used in conjunction with {@link DelegatingFilterProxy} in order + * to help with its initialization. + * + * @author Sebastien Deleuze + * @since 4.2 + * @see CORS W3C recommendation + */ +public class CorsFilter extends OncePerRequestFilter { + + private CorsProcessor processor = new DefaultCorsProcessor(); + + private final CorsConfigurationSource source; + + + /** + * Constructor accepting a {@link CorsConfigurationSource}, this source will be used + * by the filter to find the {@link CorsConfiguration} to use for each incoming request. + * @see CorsConfigurationMapping + */ + public CorsFilter(CorsConfigurationSource source) { + this.source = source; + } + + /** + * Configure a custom {@link CorsProcessor} to use to apply the matched + * {@link CorsConfiguration} for a request. + *

By default {@link DefaultCorsProcessor} is used. + */ + public void setCorsProcessor(CorsProcessor processor) { + Assert.notNull(processor, "CorsProcessor must not be null"); + this.processor = processor; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + if (CorsUtils.isCorsRequest(request)) { + CorsConfiguration corsConfiguration = this.source.getCorsConfiguration(request); + if (corsConfiguration != null) { + boolean isValid = this.processor.processRequest(corsConfiguration, request, response); + if (!isValid || CorsUtils.isPreFlightRequest(request)) { + return; + } + } + } + filterChain.doFilter(request, response); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationMappingTests.java b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationMappingTests.java new file mode 100644 index 0000000000..ff0fd3b3a1 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/cors/CorsConfigurationMappingTests.java @@ -0,0 +1,51 @@ +/* + * 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.cors; + +import static org.junit.Assert.*; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.test.MockHttpServletRequest; + +/** + * Unit tests for {@link CorsConfigurationMapping}. + * @author Sebastien Deleuze + */ +public class CorsConfigurationMappingTests { + + private final CorsConfigurationMapping mapping = new CorsConfigurationMapping(); + + @Test + public void empty() { + assertNull(this.mapping.getCorsConfiguration(new MockHttpServletRequest(HttpMethod.GET.name(), "/bar/test.html"))); + } + + @Test + public void registerAndMatch() { + CorsConfiguration config = new CorsConfiguration(); + this.mapping.registerCorsConfiguration("/bar/**", config); + assertNull(this.mapping.getCorsConfiguration(new MockHttpServletRequest(HttpMethod.GET.name(), "/foo/test.html"))); + assertEquals(config, this.mapping.getCorsConfiguration(new MockHttpServletRequest(HttpMethod.GET.name(), "/bar/test.html"))); + } + + @Test(expected = UnsupportedOperationException.class) + public void unmodifiableConfigurationsMap() { + this.mapping.getCorsConfigurations().put("/**", new CorsConfiguration()); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/filter/CorsFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/CorsFilterTests.java new file mode 100644 index 0000000000..5945010867 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/filter/CorsFilterTests.java @@ -0,0 +1,120 @@ +/* + * 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.filter; + +import java.io.IOException; +import java.util.Arrays; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; +import org.springframework.web.cors.CorsConfiguration; + +/** + * Unit tests for {@link CorsFilter}. + * @author Sebastien Deleuze + */ +public class CorsFilterTests { + + private CorsFilter filter; + + private final CorsConfiguration config = new CorsConfiguration(); + + @Before + public void setup() throws Exception { + config.setAllowedOrigins(Arrays.asList("http://domain1.com", "http://domain2.com")); + config.setAllowedMethods(Arrays.asList("GET", "POST")); + config.setAllowedHeaders(Arrays.asList("header1", "header2")); + config.setExposedHeaders(Arrays.asList("header3", "header4")); + config.setMaxAge(123L); + config.setAllowCredentials(false); + filter = new CorsFilter(r -> config); + } + + @Test + public void validActualRequest() throws ServletException, IOException { + + MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/test.html"); + request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com"); + request.addHeader("header2", "foo"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChain filterChain = (filterRequest, filterResponse) -> { + assertEquals("http://domain2.com", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertEquals("header3, header4", response.getHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)); + }; + filter.doFilter(request, response, filterChain); + } + + @Test + public void invalidActualRequest() throws ServletException, IOException { + + MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.DELETE.name(), "/test.html"); + request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com"); + request.addHeader("header2", "foo"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChain filterChain = (filterRequest, filterResponse) -> { + fail("Invalid requests must not be forwarded to the filter chain"); + }; + filter.doFilter(request, response, filterChain); + assertNull(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + + @Test + public void validPreFlightRequest() throws ServletException, IOException { + + MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.OPTIONS.name(), "/test.html"); + request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.GET.name()); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "header1, header2"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChain filterChain = (filterRequest, filterResponse) -> + fail("Preflight requests must not be forwarded to the filter chain"); + filter.doFilter(request, response, filterChain); + + assertEquals("http://domain2.com", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertEquals("header1, header2", response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS)); + assertEquals("header3, header4", response.getHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)); + assertEquals(123L, Long.parseLong(response.getHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE))); + } + + @Test + public void invalidPreFlightRequest() throws ServletException, IOException { + + MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.OPTIONS.name(), "/test.html"); + request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpMethod.DELETE.name()); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS, "header1, header2"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChain filterChain = (filterRequest, filterResponse) -> + fail("Preflight requests must not be forwarded to the filter chain"); + filter.doFilter(request, response, filterChain); + + assertNull(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index c7c78a9621..ac4873e3fc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -197,8 +197,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { configurePathMatchingProperties(handlerMappingDef, element, parserContext); - RuntimeBeanReference corsConfigurationRef = MvcNamespaceUtils.registerCorsConfiguration(null, parserContext, source); - handlerMappingDef.getPropertyValues().add("corsConfiguration", corsConfigurationRef); + RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); + handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); RuntimeBeanReference conversionService = getConversionService(element, source, parserContext); RuntimeBeanReference validator = getValidator(element, source, parserContext); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java index fa70a9e7b9..c62f70dcdf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java @@ -113,7 +113,7 @@ public class CorsBeanDefinitionParser implements BeanDefinitionParser { } } - MvcNamespaceUtils.registerCorsConfiguration(corsConfigurations, parserContext, parserContext.extractSource(element)); + MvcNamespaceUtils.registerCorsConfigurations(corsConfigurations, parserContext, parserContext.extractSource(element)); return null; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java index 5b36371d4c..e668413688 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java @@ -119,8 +119,8 @@ abstract class MvcNamespaceUtils { beanNameMappingDef.setSource(source); beanNameMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); beanNameMappingDef.getPropertyValues().add("order", 2); // consistent with WebMvcConfigurationSupport - RuntimeBeanReference corsConfigurationRef = MvcNamespaceUtils.registerCorsConfiguration(null, parserContext, source); - beanNameMappingDef.getPropertyValues().add("corsConfiguration", corsConfigurationRef); + RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); + beanNameMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); parserContext.getRegistry().registerBeanDefinition(BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME, beanNameMappingDef); parserContext.registerComponent(new BeanComponentDefinition(beanNameMappingDef, BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME)); } @@ -160,20 +160,20 @@ abstract class MvcNamespaceUtils { * if a non-null CORS configuration is provided. * @return a RuntimeBeanReference to this {@code Map} instance */ - public static RuntimeBeanReference registerCorsConfiguration(Map corsConfiguration, ParserContext parserContext, Object source) { + public static RuntimeBeanReference registerCorsConfigurations(Map corsConfigurations, ParserContext parserContext, Object source) { if (!parserContext.getRegistry().containsBeanDefinition(CORS_CONFIGURATION_BEAN_NAME)) { RootBeanDefinition corsConfigurationsDef = new RootBeanDefinition(LinkedHashMap.class); corsConfigurationsDef.setSource(source); corsConfigurationsDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - if (corsConfiguration != null) { - corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfiguration); + if (corsConfigurations != null) { + corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations); } parserContext.getReaderContext().getRegistry().registerBeanDefinition(CORS_CONFIGURATION_BEAN_NAME, corsConfigurationsDef); parserContext.registerComponent(new BeanComponentDefinition(corsConfigurationsDef, CORS_CONFIGURATION_BEAN_NAME)); } - else if (corsConfiguration != null) { + else if (corsConfigurations != null) { BeanDefinition corsConfigurationsDef = parserContext.getRegistry().getBeanDefinition(CORS_CONFIGURATION_BEAN_NAME); - corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfiguration); + corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations); } return new RuntimeBeanReference(CORS_CONFIGURATION_BEAN_NAME); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index 9ad4c4ce98..8e008ba131 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -116,8 +116,8 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { // Use a default of near-lowest precedence, still allowing for even lower precedence in other mappings handlerMappingDef.getPropertyValues().add("order", StringUtils.hasText(order) ? order : Ordered.LOWEST_PRECEDENCE - 1); - RuntimeBeanReference corsConfigurationRef = MvcNamespaceUtils.registerCorsConfiguration(null, parserContext, source); - handlerMappingDef.getPropertyValues().add("corsConfiguration", corsConfigurationRef); + RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); + handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); String beanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef); parserContext.getRegistry().registerBeanDefinition(beanName, handlerMappingDef); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java index 2843435784..af0946469b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java @@ -127,8 +127,8 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser { beanDef.getPropertyValues().add("order", "1"); beanDef.getPropertyValues().add("pathMatcher", MvcNamespaceUtils.registerPathMatcher(null, context, source)); beanDef.getPropertyValues().add("urlPathHelper", MvcNamespaceUtils.registerUrlPathHelper(null, context, source)); - RuntimeBeanReference corsConfigurationRef = MvcNamespaceUtils.registerCorsConfiguration(null, context, source); - beanDef.getPropertyValues().add("corsConfiguration", corsConfigurationRef); + RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source); + beanDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); return beanDef; } 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 65dd5fd9aa..a34b886413 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 @@ -239,7 +239,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager()); - handlerMapping.setCorsConfiguration(getCorsConfigurations()); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); PathMatchConfigurer configurer = getPathMatchConfigurer(); if (configurer.isUseSuffixPatternMatch() != null) { @@ -371,7 +371,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setPathMatcher(mvcPathMatcher()); handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); handlerMapping.setInterceptors(getInterceptors()); - handlerMapping.setCorsConfiguration(getCorsConfigurations()); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); return handlerMapping; } @@ -391,7 +391,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping(); mapping.setOrder(2); mapping.setInterceptors(getInterceptors()); - mapping.setCorsConfiguration(getCorsConfigurations()); + mapping.setCorsConfigurations(getCorsConfigurations()); return mapping; } @@ -411,7 +411,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); handlerMapping.setInterceptors(new HandlerInterceptor[] { new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider())}); - handlerMapping.setCorsConfiguration(getCorsConfigurations()); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); } else { handlerMapping = new EmptyHandlerMapping(); 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 d096216fb1..23cf5b78c0 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,6 @@ package org.springframework.web.servlet.handler; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; @@ -29,6 +28,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.core.Ordered; import org.springframework.web.HttpRequestHandler; +import org.springframework.web.cors.CorsConfigurationMapping; import org.springframework.web.cors.CorsProcessor; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; @@ -81,8 +81,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport private CorsProcessor corsProcessor = new DefaultCorsProcessor(); - private final Map corsConfiguration = - new LinkedHashMap(); + private final CorsConfigurationMapping corsMapping = new CorsConfigurationMapping(); /** @@ -125,6 +124,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + this.corsMapping.setAlwaysUseFullPath(alwaysUseFullPath); } /** @@ -136,6 +136,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setUrlDecode(boolean urlDecode) { this.urlPathHelper.setUrlDecode(urlDecode); + this.corsMapping.setUrlDecode(urlDecode); } /** @@ -145,6 +146,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setRemoveSemicolonContent(boolean removeSemicolonContent) { this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); + this.corsMapping.setRemoveSemicolonContent(removeSemicolonContent); } /** @@ -156,6 +158,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport public void setUrlPathHelper(UrlPathHelper urlPathHelper) { Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); this.urlPathHelper = urlPathHelper; + this.corsMapping.setUrlPathHelper(urlPathHelper); } /** @@ -173,6 +176,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport public void setPathMatcher(PathMatcher pathMatcher) { Assert.notNull(pathMatcher, "PathMatcher must not be null"); this.pathMatcher = pathMatcher; + this.corsMapping.setPathMatcher(pathMatcher); } /** @@ -221,18 +225,15 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport * handler, if any. * @since 4.2 */ - public void setCorsConfiguration(Map corsConfiguration) { - this.corsConfiguration.clear(); - if (corsConfiguration != null) { - this.corsConfiguration.putAll(corsConfiguration); - } + public void setCorsConfigurations(Map corsConfigurations) { + this.corsMapping.setCorsConfigurations(corsConfigurations); } /** * Get the CORS configuration. */ - public Map getCorsConfiguration() { - return this.corsConfiguration; + public Map getCorsConfigurations() { + return this.corsMapping.getCorsConfigurations(); } /** @@ -361,7 +362,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (CorsUtils.isCorsRequest(request)) { - CorsConfiguration globalConfig = getCorsConfiguration(request); + CorsConfiguration globalConfig = this.corsMapping.getCorsConfiguration(request); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); @@ -378,7 +379,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 #getCorsConfiguration}, + * the CORS configuration to be obtained via {@link #getCorsConfigurations}, * *

Note: This method may also return a pre-built {@link HandlerExecutionChain}, * combining a handler object with dynamically determined interceptors. @@ -429,22 +430,6 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport return chain; } - /** - * Find the "global" CORS configuration for the given URL configured via - * {@link #setCorsConfiguration(Map)}. - * @param request the request - * @return the CORS configuration or {@code null} - */ - protected CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); - for(Map.Entry entry : getCorsConfiguration().entrySet()) { - if (getPathMatcher().match(entry.getKey(), lookupPath)) { - return entry.getValue(); - } - } - return null; - } - /** * Retrieve the CORS configuration for the given handler. * @param handler the handler to check (never {@code null}). 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 dca9ce9230..fe7ba2f77f 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 @@ -875,7 +875,7 @@ public class MvcNamespaceTests { for (String beanName : beanNames) { AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName); assertNotNull(handlerMapping); - Map configs = handlerMapping.getCorsConfiguration(); + Map configs = handlerMapping.getCorsConfigurations(); assertNotNull(configs); assertEquals(1, configs.size()); CorsConfiguration config = configs.get("/**"); @@ -898,7 +898,7 @@ public class MvcNamespaceTests { for (String beanName : beanNames) { AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName); assertNotNull(handlerMapping); - Map configs = handlerMapping.getCorsConfiguration(); + Map configs = handlerMapping.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 31fece4e53..0a6c680ee7 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 @@ -118,7 +118,7 @@ public class CorsAbstractHandlerMappingTests { public void actualRequestWithMappedCorsConfiguration() throws Exception { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); - this.handlerMapping.setCorsConfiguration(Collections.singletonMap("/foo", config)); + this.handlerMapping.setCorsConfigurations(Collections.singletonMap("/foo", config)); this.request.setMethod(RequestMethod.GET.name()); this.request.setRequestURI("/foo"); this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com"); @@ -135,7 +135,7 @@ public class CorsAbstractHandlerMappingTests { public void preflightRequestWithMappedCorsConfiguration() throws Exception { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); - this.handlerMapping.setCorsConfiguration(Collections.singletonMap("/foo", config)); + this.handlerMapping.setCorsConfigurations(Collections.singletonMap("/foo", config)); this.request.setMethod(RequestMethod.OPTIONS.name()); this.request.setRequestURI("/foo"); this.request.addHeader(HttpHeaders.ORIGIN, "http://domain2.com");