Support CORS global configuration in XML namespace

This commit introduces support for this kind of CORS XML namespace configuration:

	<mvc:cors>

		<mvc:mapping path="/api/**"
					allowed-origins="http://domain1.com, http://domain2.com"
					allowed-methods="GET, PUT"
					allowed-headers="header1, header2, header3"
					exposed-headers="header1, header2" allow-credentials="false"
					max-age="123" />

		<mvc:mapping path="/resources/**" allowed-origins="http://domain1.com" />

	</mvc:cors>

Issue: SPR-13046
This commit is contained in:
Sebastien Deleuze
2015-06-02 16:11:56 +02:00
parent 4f1286a4c3
commit e5f76af193
10 changed files with 350 additions and 17 deletions

View File

@@ -197,6 +197,9 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
configurePathMatchingProperties(handlerMappingDef, element, parserContext);
RuntimeBeanReference corsConfigurationRef = MvcNamespaceUtils.registerCorsConfiguration(null, parserContext, source);
handlerMappingDef.getPropertyValues().add("corsConfiguration", corsConfigurationRef);
RuntimeBeanReference conversionService = getConversionService(element, source, parserContext);
RuntimeBeanReference validator = getValidator(element, source, parserContext);
RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element);

View File

@@ -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.servlet.config;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.http.HttpMethod;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.springframework.web.cors.CorsConfiguration;
/**
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a
* {@code cors} element in order to set the CORS configuration in the various
* {AbstractHandlerMapping} beans created by {@link AnnotationDrivenBeanDefinitionParser},
* {@link ResourcesBeanDefinitionParser} and {@link ViewControllerBeanDefinitionParser}.
*
* @author Sebastien Deleuze
* @since 4.2
*/
public class CorsBeanDefinitionParser implements BeanDefinitionParser {
private static final List<String> DEFAULT_ALLOWED_ORIGINS = Arrays.asList("*");
private static final List<String> DEFAULT_ALLOWED_METHODS =
Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
private static final List<String> DEFAULT_ALLOWED_HEADERS = Arrays.asList("*");
private static final boolean DEFAULT_ALLOW_CREDENTIALS = true;
private static final long DEFAULT_MAX_AGE = 1600;
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<String, CorsConfiguration>();
List<Element> mappings = DomUtils.getChildElementsByTagName(element, "mapping");
if (mappings.isEmpty()) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(DEFAULT_ALLOWED_ORIGINS);
config.setAllowedMethods(DEFAULT_ALLOWED_METHODS);
config.setAllowedHeaders(DEFAULT_ALLOWED_HEADERS);
config.setAllowCredentials(DEFAULT_ALLOW_CREDENTIALS);
config.setMaxAge(DEFAULT_MAX_AGE);
corsConfigurations.put("/**", config);
}
else {
for (Element mapping : mappings) {
CorsConfiguration config = new CorsConfiguration();
if (mapping.hasAttribute("allowed-origins")) {
String[] allowedOrigins = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-origins"), ",");
config.setAllowedOrigins(Arrays.asList(allowedOrigins));
}
else {
config.setAllowedOrigins(DEFAULT_ALLOWED_ORIGINS);
}
if (mapping.hasAttribute("allowed-methods")) {
String[] allowedMethods = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-methods"), ",");
config.setAllowedMethods(Arrays.asList(allowedMethods));
}
else {
config.setAllowedMethods(DEFAULT_ALLOWED_METHODS);
}
if (mapping.hasAttribute("allowed-headers")) {
String[] allowedHeaders = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-headers"), ",");
config.setAllowedHeaders(Arrays.asList(allowedHeaders));
}
else {
config.setAllowedHeaders(DEFAULT_ALLOWED_HEADERS);
}
if (mapping.hasAttribute("exposed-headers")) {
String[] exposedHeaders = StringUtils.tokenizeToStringArray(mapping.getAttribute("exposed-headers"), ",");
config.setExposedHeaders(Arrays.asList(exposedHeaders));
}
if (mapping.hasAttribute("allow-credentials")) {
config.setAllowCredentials(Boolean.parseBoolean(mapping.getAttribute("allow-credentials")));
}
else {
config.setAllowCredentials(DEFAULT_ALLOW_CREDENTIALS);
}
if (mapping.hasAttribute("max-age")) {
config.setMaxAge(Long.parseLong(mapping.getAttribute("max-age")));
}
else {
config.setMaxAge(DEFAULT_MAX_AGE);
}
corsConfigurations.put(mapping.getAttribute("path"), config);
}
}
MvcNamespaceUtils.registerCorsConfiguration(corsConfigurations, parserContext, parserContext.extractSource(element));
return null;
}
}

View File

@@ -44,6 +44,7 @@ public class MvcNamespaceHandler extends NamespaceHandlerSupport {
registerBeanDefinitionParser("velocity-configurer", new VelocityConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("script-template-configurer", new ScriptTemplateConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("cors", new CorsBeanDefinitionParser());
}
}

View File

@@ -16,6 +16,9 @@
package org.springframework.web.servlet.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
@@ -23,6 +26,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
@@ -50,6 +54,8 @@ abstract class MvcNamespaceUtils {
private static final String PATH_MATCHER_BEAN_NAME = "mvcPathMatcher";
private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations";
public static void registerDefaultComponents(ParserContext parserContext, Object source) {
registerBeanNameUrlHandlerMapping(parserContext, source);
@@ -113,6 +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);
parserContext.getRegistry().registerBeanDefinition(BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME, beanNameMappingDef);
parserContext.registerComponent(new BeanComponentDefinition(beanNameMappingDef, BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME));
}
@@ -146,4 +154,28 @@ abstract class MvcNamespaceUtils {
}
}
/**
* Registers a {@code Map<String, CorsConfiguration>} (mapped {@code CorsConfiguration}s)
* under a well-known name unless already registered. The bean definition may be updated
* if a non-null CORS configuration is provided.
* @return a RuntimeBeanReference to this {@code Map<String, CorsConfiguration>} instance
*/
public static RuntimeBeanReference registerCorsConfiguration(Map<String, CorsConfiguration> corsConfiguration, 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);
}
parserContext.getReaderContext().getRegistry().registerBeanDefinition(CORS_CONFIGURATION_BEAN_NAME, corsConfigurationsDef);
parserContext.registerComponent(new BeanComponentDefinition(corsConfigurationsDef, CORS_CONFIGURATION_BEAN_NAME));
}
else if (corsConfiguration != null) {
BeanDefinition corsConfigurationsDef = parserContext.getRegistry().getBeanDefinition(CORS_CONFIGURATION_BEAN_NAME);
corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfiguration);
}
return new RuntimeBeanReference(CORS_CONFIGURATION_BEAN_NAME);
}
}

View File

@@ -116,6 +116,9 @@ 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);
String beanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef);
parserContext.getRegistry().registerBeanDefinition(beanName, handlerMappingDef);
parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, beanName));

View File

@@ -22,6 +22,7 @@ import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -126,6 +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);
return beanDef;
}