The recent Spring Framework 4.2 RC1 release has introduced CORS first class support. It provides fine grained CORS support thanks to the @CrossOrigin
annotation (nothing to do on Spring Boot side to support that except depending on Spring Framework 4.2 ...), but that may be interesting to provide a Spring Boot alternative to the CorsConfigurer
Spring Framework JavaConfig API.
A key class in Spring Framework CORS support is CorsConfiguration
, it allows to specify all the CORS properties to use. My proposal would be to add Spring Boot support for this kind of declaration for CORS global configuration:
@Bean
CorsConfiguration cors() {
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("header1");
}
For supporting mapped CORS configuration (since Spring Framework 4.2 support them), what about using bean names to specify the path pattern to use:
@Bean(name = "/api")
CorsConfiguration corsForApi() {
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("header1");
}
@Bean(name = "/resources")
CorsConfiguration corsForResources() {
config.addAllowedOrigin("http://domain2.com");
config.addAllowedHeader("header2");
}
These beans could then be retrieved by Spring Boot Cors auto-configuration facilities in a MapAbstractHandlerMapping
instances.
Any thoughts ?
Comment From: snicoll
A couple of remarks:
1. We could reuse the configuration keys infrastructure to map the default CorsConfiguration
instead of a bean definition (spring.mvc.cors.default
or something)
2. I wouldn't use the name to specify the path pattern to use. What If you have XYZConfiguration tomorrow that applies to path pattern as well?
Configuring the default cors configuration via the Environment
seems indeed a Spring Boot specific. I am not sure about the other one. What would you do in plain Spring Framework?
Comment From: sdeleuze
- Indeed, reusing the configuration keys infrastructure is an interesting alternative. Even with multiple mappings. Maybe something like
spring.mvc.cors.api.allowed-origins = http://domain1.com
would map the CORS configuration to/api/**
, with the possibility to provide a customized path pattern with thespring.mvc.cors.api.path
key. - Indeed, there will be an issue if they use the same name.
In plain Spring Framework, you will do:
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void configureCors(CorsConfigurer configurer) {
configurer.enableCors("/api/**")
.allowedOrigins("http://domain1.com")
.allowedHeaders("header1");
configurer.enableCors("resources/**")
.allowedOrigins("http://domain2.com")
.allowedHeaders("header2");
}
}
Comment From: sdeleuze
Based on @snicoll feedback and more thoughts, my proposal is now:
Supporting only global CORS configuration based on CorsConfiguration
bean declaration:
@Bean
CorsConfiguration cors() {
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("header1");
}
Supporting these 3 kinds of mapping thanks to Spring Boot configuration keys infrastructure:
# Default mapping on /**
spring.mvc.cors.allowed-origins = http://domain.com
spring.mvc.cors.allowed-methods = GET, POST
spring.mvc.cors.allowed-headers = header1, header2
spring.mvc.cors.exposed-headers = header1, header2
spring.mvc.cors.max-age = 3600
spring.mvc.cors.alllow-credentials = false
# Mapping on /foo/**
spring.mvc.cors.foo.allowed-origins = http://domain.com
spring.mvc.cors.foo.allowed-methods = GET, POST
spring.mvc.cors.foo.allowed-headers = header1, header2
spring.mvc.cors.foo.exposed-headers = header1, header2
spring.mvc.cors.foo.max-age = 3600
spring.mvc.cors.foo.alllow-credentials = false
# Custom mapping on /**/*.json
spring.mvc.cors.bar.path = /**/*.json
spring.mvc.cors.bar.allowed-origins = http://domain.com
spring.mvc.cors.bar.allowed-methods = GET, POST
spring.mvc.cors.bar.allowed-headers = header1, header2
spring.mvc.cors.bar.exposed-headers = header1, header2
spring.mvc.cors.bar.max-age = 3600
spring.mvc.cors.bar.alllow-credentials = false
Comment From: snicoll
I am still having a hard time with this. All I can see here points to the direction of a registry
that would map a path to a CorsConfiguration
. I don't like the special case being mixed with a "per-name" mapping. /**
. Don't forget that if you have a Map<String,CorsConfiguration
in configuration properties, you need to give him a dedicated namespace (since it's eating *
).
Are users going to be willing to tune this via configuration or are they going to configure that in code? If they have several CorsConfiguration
they may want to share some of these settings. With the properties-based solution, you'd have to copy/paste (as in your example).
I guess what I am saying is that I see little value of "simply" exposing the core contract to configuration. But again, I don't know how users are going to use this so more info on that may help figuring out the right level of customization. Thanks!
Comment From: snicoll
@sdeleuze and I had a call on this and I see now what we're after. So to summarize what we discussed (which answers my concerns above).
The general MvcCorsProperties
may look like the following (simplified)
@ConfigurationProperties("spring.mvc.cors")
public class MvcCorsProperties {
// property javadoc
private final Map<String,PathCorsConfiguration> mappings ...
public Map<String,PathCorsConfiguration> getMappings();
}
This allows extra cors-related configuration properties to be defined in the future. PathCorsConfiguration
could be an extension of CorsConfiguration
that exposes the path
property.
That map has a special key called default
. If one registers a PathCorsConfiguration
for that key, this will configure the default cors configuration. If the user creates extra configuration (for dedicated mappings), any property that is not set will actually use the default value. This should reduce the copy-paste significantly.
We may want to revisit MvcEndpointCorsProperties
as it's only doing a mapping for the sake of having proper meta-data (description for each property). I think it's interesting that we keep that so we could extract that so that this feature can benefit from it as well.
Comment From: sdeleuze
@snicoll Maybe for @Bean
based CORS configuration we could support CorsRegistry
beans.
It is originally designed to be used in @EnableWebMvc
Spring MVC configuration, but it has a nice fluent API designed to define mapped CORS configuration and could maybe be reused in Spring Boot if we make CorsRegistry#getCorsConfigurations ()
public.
Comment From: snicoll
Given the amount of code, I don't think it matters much. I am still not sure in which camp I sit. There are two options:
- Detect the presence of a CorsRegistry
bean and basically use that to "auto-configure" the CORS support
- Expose a @ConfigurationProperties
pojo with a Map<String,CorsConfiguration>
Doing both will be confusing IMO.
Comment From: sdeleuze
Maybe:
- Simple case : a @ConfigurationProperties pojo with a CorsConfiguration
that will be mapped for all the application (/**
)
- More advanced cases will be configured with a CorsRegistry
bean
Comment From: wilkinsona
I agree with @snicoll that doing both will be confusing. We also need to make sure that whatever we do here doesn't lead to confusion with the Actuator's new CORS support.
I'm not keen on the @ConfigurationProperties
Map<String,CorsConfiguration>
approach. It feels like programming via properties/yaml to me. Auto-configuring a CorsRegistry
bean is, imo, the most Spring Boot-like approach that's been described thus far but I'm not really sure that it's that much better than Spring Framework's existing API for configuring CORS and there's a lot to be said for only having a single way to do something.
Comment From: sdeleuze
In fact, what I like in the CorsRegistry
bean solution is that would be the same API, the only thing we will add is the possibility to use it in Spring Boot applications without having to disable the web autoconfiguration (= without having to use @EnableWebMvc
).
Basically the same API but natively integrated in Spring Boot.
Comment From: snicoll
Then we should detect if a bean of type CorsRegistry
is present in the context and if so auto-bind it to our configuration. Would that work for you?
Comment From: sdeleuze
:+1:
Basically that means generating a Map<String,CorsConfiguration>
using the CorsRegistry
bean, and use it to set the corsConfiguration
properties of all AbstractHandlerMapping
instances created by Spring Boot (exactly what we do in WebMvcConfigurationSupport
).
I will send a PR if you and @wilkinsona are ok.
Comment From: wilkinsona
Sounds good to me
Comment From: sdeleuze
After discussing with @rstoyanchev and @wilkinsona, it appears that we have already a way to declare CORS configuration without disabling Spring Boot web autoconfiguration (so for this part, I guess updating the documentation will be enough) by declaring a WebMvcConfigurer
bean:
@Bean
public WebMvcConfigurer mvcConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "PUT");
}
};
}
But I wonder if, in addition to these advanced capabilities, we should provide a more simple global CORS configuration by mapping CorsConfiguration
on /**
, in order to provide an easier syntax for people with simple needs (enabling CORS on "/**" is a very common use case):
@Bean
public CorsConfiguration corsConfig() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
return config;
}
Any thoughts ?
Comment From: wilkinsona
I'm not keen on providing two similar ways to do the same thing. Also, I think the CorsConfiguration
@Bean
approach isn't as expressive as using a WebMvcConfigurerAdapter
– it's not clear looking at the code which paths the CORS configuration will be applied to. In short, I'm a big fan of Spring Framework's API for configuring CORS and I don't think Boot needs to do anything special here.
Comment From: sdeleuze
Ok thanks, I will just update the documentation.
Comment From: sdeleuze
Could you have a look to the pull request https://github.com/spring-projects/spring-boot/pull/3523 I have just submitted?
Comment From: lpetit-yseop
Hello @sdeleuze @snicoll @wilkinsona, with the current state of the code, if one wants to leverage Spring Boot properties to override CorsConfiguration default values, one needs to do something like this:
@Bean
@ConfigurationProperties("cors")
public CorsConfiguration corsConfiguration() {
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.applyPermitDefaultValues();
return corsConfiguration;
}
@Bean
public WebMvcConfigurer corsConfigurer(CorsConfiguration globalCorsConfiguration) {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
final CorsRegistration registration = registry.addMapping("/**");
combine(registration, globalCorsConfiguration);
}
};
}
/**
* Combines the registration's {@link CorsConfiguration} with {@code corsConfiguration},
* as if {@link CorsConfiguration#combine(CorsConfiguration)} had been called on the registration's
* {@link CorsConfiguration}.
* <p>
* This method is required since {@link CorsRegistration#getCorsConfiguration()} is not public,
* it cannot be called and thus {@link CorsConfiguration#combine(CorsConfiguration)} cannot be called.
*/
void combine(CorsRegistration registration, CorsConfiguration corsConfiguration) {
if (corsConfiguration.getAllowCredentials() != null) {
registration.allowCredentials(corsConfiguration.getAllowCredentials());
}
if (corsConfiguration.getAllowedHeaders() != null) {
registration
.allowedHeaders(corsConfiguration.getAllowedHeaders().toArray(new String[corsConfiguration.getAllowedHeaders().size()]));
}
if (corsConfiguration.getAllowedMethods() != null) {
registration
.allowedMethods(corsConfiguration.getAllowedMethods().toArray(new String[corsConfiguration.getAllowedHeaders().size()]));
}
if (corsConfiguration.getAllowedOrigins() != null) {
registration
.allowedOrigins(corsConfiguration.getAllowedOrigins().toArray(new String[corsConfiguration.getAllowedHeaders().size()]));
}
if (corsConfiguration.getExposedHeaders() != null) {
registration
.exposedHeaders(corsConfiguration.getExposedHeaders().toArray(new String[corsConfiguration.getAllowedHeaders().size()]));
}
if (corsConfiguration.getMaxAge() != null) {
registration.maxAge(corsConfiguration.getMaxAge());
}
}
whereas, if the CorsRegistration#getCorsConfiguration()
method was made public, one could just write this:
@Bean
@ConfigurationProperties("cors")
public CorsConfiguration corsConfiguration() {
final CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.applyPermitDefaultValues();
return corsConfiguration;
}
@Bean
public WebMvcConfigurer corsConfigurer(CorsConfiguration globalCorsConfiguration) {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
// Design 1: with CorsRegistration#getCorsConfiguration() made public instead of protected
final CorsRegistration registration = registry.addMapping("/**");
registration.getCorsConfiguration().combine(globalCorsConfiguration);
// Design 2: with an additional CorsRegistration#combineConfiguration() method
registry.addMapping("/**").combineConfiguration(globalCorsConfiguration);
}
};
}
which would be a big win, IMHO.
What do you think? Would you consider changing the visibility of the CorsRegistration#getCorsConfiguration()
method? Or add a new CorsRegistration#combine()
method that would delegate to the CorsConfiguration#combine()
method and return the CorsRegistration
to not break the fluent API paradigm?
Comment From: snicoll
@lpetit-yseop thanks for the feedback but this issue is closed and CorsRegistration
is part of Spring Framework. If you want to suggest an enhancement, please open a separate issue in the Spring Framework issue tracker.
Comment From: lpetit-yseop
@lpetit-yseop thanks for the feedback but this issue is closed and
CorsRegistration
is part of Spring Framework. If you want to suggest an enhancement, please open a separate issue in the Spring Framework issue tracker.
Thank you for the suggestion. Issue created there: https://github.com/spring-projects/spring-framework/issues/25716