Add global CORS configuration capabilities

This commit adds JavaConfig based global CORS configuration
capabilities to Spring MVC. It is now possible to specify
multiple CORS configurations, each mapped on a path pattern,
by overriding
WebMvcConfigurerAdapter#configureCrossOrigin(CrossOriginConfigurer).

It is also possible to combine global and @CrossOrigin based
CORS configuration.

Issue: SPR-12933
This commit is contained in:
Sebastien Deleuze
2015-05-16 00:52:51 +02:00
parent 696a010e81
commit 49fff7513d
14 changed files with 548 additions and 13 deletions

View File

@@ -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<CrossOriginRegistration> registrations = new ArrayList<CrossOriginRegistration>();
/**
* Enable cross origin requests on the specified path patterns. If no path pattern is specified,
* cross-origin request handling is mapped on "/**" .
*
* <p>By default, all origins, all headers and credentials are allowed. Max age is set to 30 minutes.</p>
*/
public CrossOriginRegistration enableCrossOrigin(String... pathPatterns) {
CrossOriginRegistration registration = new CrossOriginRegistration(pathPatterns);
this.registrations.add(registration);
return registration;
}
protected Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> configs = new HashMap<String, CorsConfiguration>();
for (CrossOriginRegistration registration : this.registrations) {
for (String pathPattern : registration.getPathPatterns()) {
configs.put(pathPattern, registration.getCorsConfiguration());
}
}
return configs;
}
}

View File

@@ -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 "/**" .
*
* <p>By default, all origins, all headers, credentials and GET, HEAD, POST methods are allowed.
* Max age is set to 30 minutes.</p>
*
* @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<String>(Arrays.asList(origins)));
return this;
}
public CrossOriginRegistration allowedMethods(String... methods) {
this.config.setAllowedMethods(new ArrayList<String>(Arrays.asList(methods)));
return this;
}
public CrossOriginRegistration allowedHeaders(String... headers) {
this.config.setAllowedHeaders(new ArrayList<String>(Arrays.asList(headers)));
return this;
}
public CrossOriginRegistration exposedHeaders(String... headers) {
this.config.setExposedHeaders(new ArrayList<String>(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;
}
}

View File

@@ -132,4 +132,9 @@ public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
this.configurers.configureHandlerExceptionResolvers(exceptionResolvers);
}
@Override
protected void configureCrossOrigin(CrossOriginConfigurer configurer) {
this.configurers.configureCrossOrigin(configurer);
}
}

View File

@@ -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<HttpMessageConverter<?>> messageConverters;
private Map<String, CorsConfiguration> 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<String, CorsConfiguration> 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 {

View File

@@ -182,4 +182,10 @@ public interface WebMvcConfigurer {
*/
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);
/**
* Configure cross-origin requests handling.
* @since 4.2
*/
void configureCrossOrigin(CrossOriginConfigurer configurer);
}

View File

@@ -165,4 +165,12 @@ public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
/**
* {@inheritDoc}
* <p>This implementation is empty.
*/
@Override
public void configureCrossOrigin(CrossOriginConfigurer configurer) {
}
}

View File

@@ -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> T selectSingleInstance(List<T> instances, Class<T> instanceType) {
if (instances.size() > 1) {
throw new IllegalStateException(

View File

@@ -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<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>();
private CorsProcessor corsProcessor = new DefaultCorsProcessor();
private Map<String, CorsConfiguration> corsConfigurations = new HashMap<String, CorsConfiguration>();
/**
@@ -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. <p>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<String, CorsConfiguration> 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<String, CorsConfiguration> 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)) {

View File

@@ -436,17 +436,18 @@ public abstract class AbstractHandlerMethodMapping<T> 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;
}