From e011312c68ef43e615e475874b8cd4ded3b32a04 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Thu, 30 May 2013 15:37:49 +0100 Subject: [PATCH] [bs-138] Make it easy to secure only the management endpoints Example: web UI with publicly available static assets # application.properties: security.ignored: /css/**,/script/** Example: web UI with publicly available everything, but secure management endpoints. # application.properties: # Empty path for basic security (default is /**) security.basic.path= [Fixes #50721675] --- spring-bootstrap-actuator/docs/Features.md | 35 ++++++++- .../ManagementServerConfiguration.java | 38 ++++++++++ .../SecurityAutoConfiguration.java | 71 ++++++++++++++++--- .../properties/SecurityProperties.java | 11 +-- .../ManagementConfigurationTests.java | 9 +++ .../properties/SecurityPropertiesTests.java | 55 ++++++++++++++ ...ingBootstrapCompilerAutoConfiguration.java | 1 + ...dressServiceBootstrapApplicationTests.java | 6 +- .../ui/ActuatorUiBootstrapApplication.java | 29 +++----- .../ActuatorUiBootstrapApplicationTests.java | 9 +++ .../PropertySourcesBindingPostProcessor.java | 8 ++- .../EnableConfigurationPropertiesTests.java | 18 +++++ 12 files changed, 244 insertions(+), 46 deletions(-) create mode 100644 spring-bootstrap-actuator/src/test/java/org/springframework/bootstrap/actuate/properties/SecurityPropertiesTests.java diff --git a/spring-bootstrap-actuator/docs/Features.md b/spring-bootstrap-actuator/docs/Features.md index de4a9a2e08..ca515f6192 100644 --- a/spring-bootstrap-actuator/docs/Features.md +++ b/spring-bootstrap-actuator/docs/Features.md @@ -168,6 +168,27 @@ a jar which wraps `SpringApplication`: $ java -jar myproject.jar --spring.config.name=myproject +## Providing Defaults for Externalized Configuration + +For `@ConfigurationProperties` beans that are provided by the +framework itself you can always change the values that are bound to it +by changing `application.properties`. But it is sometimes also useful +to change the default values imperatively in Java, so get more control +over the process. You can do this by declaring a bean of the same +type in your application context, e.g. for the server properties: + + @AssertMissingBean(ServerProperties.class) + @Bean + public ServerProperties serverProperties() { + ServerProperties server = new ServerProperties(); + server.setPort(8888); + return server; + } + +Note the use of `@AssertMissingBean` to guard against any mistakes +where the bean is already defined (and therefore might already have +been bound). + ## Server Configuration The `ServerProperties` are bound to application properties, and @@ -337,9 +358,13 @@ every request in the main server (and the management server if it is running on the same port). There is a single account by default, and you can test it like this: - $ mvn user:password@localhost:8080/info + $ mvn user:password@localhost:8080/metrics ... stuff comes out +If the management server is running on a different port it is +unsecured by default. If you want to secure it you can add a security +auto configuration explicitly + ## Security - HTTPS Ensuring that all your main endpoints are only available over HTTPS is @@ -357,10 +382,14 @@ entries to `application.properties`, e.g. server.tomcat.remote_ip_header: x-forwarded-for server.tomcat.protocol_header: x-forwarded-proto -(Or you can add the `RemoteIpValve` yourself by adding a +(The presence of either of those properties will switch on the +valve. Or you can add the `RemoteIpValve` yourself by adding a `TomcatEmbeddedServletContainerFactory` bean.) -TODO: Spring Security configuration for 'require channel'. +Spring Security can also be configured to require a secure channel for +all (or some requests). To switch that on in an Actuator application +you just need to set `security.require_https: true` in +`application.properties`. ## Audit Events diff --git a/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/autoconfigure/ManagementServerConfiguration.java b/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/autoconfigure/ManagementServerConfiguration.java index f5e2b3f9fb..f407f74143 100644 --- a/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/autoconfigure/ManagementServerConfiguration.java +++ b/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/autoconfigure/ManagementServerConfiguration.java @@ -16,12 +16,22 @@ package org.springframework.bootstrap.actuate.autoconfigure; +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.bootstrap.actuate.endpoint.error.ErrorEndpoint; import org.springframework.bootstrap.actuate.properties.ManagementServerProperties; import org.springframework.bootstrap.context.annotation.ConditionalOnBean; +import org.springframework.bootstrap.context.annotation.ConditionalOnClass; import org.springframework.bootstrap.context.embedded.ConfigurableEmbeddedServletContainerFactory; import org.springframework.bootstrap.context.embedded.EmbeddedServletContainerCustomizer; import org.springframework.bootstrap.context.embedded.EmbeddedServletContainerFactory; @@ -31,8 +41,10 @@ import org.springframework.bootstrap.context.embedded.tomcat.TomcatEmbeddedServl import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -43,6 +55,7 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; */ @Configuration @EnableWebMvc +@Import(ManagementSecurityConfiguration.class) public class ManagementServerConfiguration { @Bean @@ -100,3 +113,28 @@ public class ManagementServerConfiguration { } } + +@Configuration +@ConditionalOnClass(name = { + "org.springframework.security.config.annotation.web.EnableWebSecurity", + "javax.servlet.Filter" }) +class ManagementSecurityConfiguration { + + @Bean + // TODO: enable and get rid of the empty filter when @ConditionalOnBean works + // @ConditionalOnBean(name = "springSecurityFilterChain") + public Filter springSecurityFilterChain(HierarchicalBeanFactory beanFactory) { + BeanFactory parent = beanFactory.getParentBeanFactory(); + if (parent != null && parent.containsBean("springSecurityFilterChain")) { + return parent.getBean("springSecurityFilterChain", Filter.class); + } + return new GenericFilterBean() { + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + chain.doFilter(request, response); + } + }; + } + +} diff --git a/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/autoconfigure/SecurityAutoConfiguration.java b/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/autoconfigure/SecurityAutoConfiguration.java index 627398a3be..6b9904f4fa 100644 --- a/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/autoconfigure/SecurityAutoConfiguration.java +++ b/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/autoconfigure/SecurityAutoConfiguration.java @@ -16,6 +16,11 @@ package org.springframework.bootstrap.actuate.autoconfigure; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.bootstrap.actuate.properties.EndpointsProperties; import org.springframework.bootstrap.actuate.properties.SecurityProperties; @@ -23,11 +28,16 @@ import org.springframework.bootstrap.context.annotation.ConditionalOnClass; import org.springframework.bootstrap.context.annotation.ConditionalOnMissingBean; import org.springframework.bootstrap.context.annotation.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.config.BeanIds; import org.springframework.security.config.annotation.authentication.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.EnableWebSecurity; import org.springframework.security.config.annotation.web.HttpConfiguration; @@ -58,6 +68,7 @@ public class SecurityAutoConfiguration { } @Bean + @ConditionalOnMissingBean({ BoostrapWebSecurityConfigurerAdapter.class }) public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() { return new BoostrapWebSecurityConfigurerAdapter(); } @@ -76,20 +87,42 @@ public class SecurityAutoConfiguration { @Override protected void configure(HttpConfiguration http) throws Exception { + if (this.security.isRequireSsl()) { - http.requiresChannel().antMatchers("/**").requiresSecure(); + http.requiresChannel().anyRequest().requiresSecure(); } + if (this.security.getBasic().isEnabled()) { - HttpConfiguration matcher = http.antMatcher(this.security.getBasic() - .getPath()); - matcher.authenticationEntryPoint(entryPoint()).antMatcher("/**") - .httpBasic().authenticationEntryPoint(entryPoint()).and() - .anonymous().disable(); - matcher.authorizeUrls().antMatchers("/**") + + String[] paths = getSecurePaths(); + + HttpConfiguration matcher = http.requestMatchers().antMatchers(paths); + matcher.authenticationEntryPoint(entryPoint()).httpBasic() + .authenticationEntryPoint(entryPoint()).and().anonymous() + .disable(); + matcher.authorizeUrls().anyRequest() .hasRole(this.security.getBasic().getRole()); + } + // No cookies for service endpoints by default http.sessionManagement().sessionCreationPolicy(this.security.getSessions()); + + } + + private String[] getSecurePaths() { + List list = new ArrayList(); + for (String path : this.security.getBasic().getPath()) { + path = path == null ? "" : path.trim(); + if (path.equals("/**")) { + return new String[] { path }; + } + if (!path.equals("")) { + list.add(path); + } + } + list.addAll(Arrays.asList(this.endpoints.getSecurePaths())); + return list.toArray(new String[list.size()]); } private AuthenticationEntryPoint entryPoint() { @@ -100,9 +133,8 @@ public class SecurityAutoConfiguration { @Override public void configure(WebSecurityBuilder builder) throws Exception { - builder.ignoring().antMatchers(this.endpoints.getHealth().getPath(), - this.endpoints.getInfo().getPath(), - this.endpoints.getError().getPath()); + builder.ignoring().antMatchers(this.security.getIgnored()) + .antMatchers(this.endpoints.getOpenPaths()); } @Override @@ -117,7 +149,7 @@ public class SecurityAutoConfiguration { } - @ConditionalOnMissingBean(AuthenticationManager.class) + @Conditional(NoUserSuppliedAuthenticationManager.class) @Configuration public static class AuthenticationManagerConfiguration { @@ -130,4 +162,21 @@ public class SecurityAutoConfiguration { } + private static class NoUserSuppliedAuthenticationManager implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + String[] beans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + context.getBeanFactory(), AuthenticationManager.class, false, false); + for (String bean : beans) { + if (!BeanIds.AUTHENTICATION_MANAGER.equals(bean)) { + // Not the one supplied by Spring Security automatically + return false; + } + } + return true; + } + + } + } diff --git a/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/properties/SecurityProperties.java b/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/properties/SecurityProperties.java index f6b2e8b816..5e63831b04 100644 --- a/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/properties/SecurityProperties.java +++ b/spring-bootstrap-actuator/src/main/java/org/springframework/bootstrap/actuate/properties/SecurityProperties.java @@ -33,7 +33,8 @@ public class SecurityProperties { private SessionCreationPolicy sessions = SessionCreationPolicy.stateless; - private String[] ignored = new String[0]; + private String[] ignored = new String[] { "/css/**", "/js/**", "/images/**", + "/**/favicon.ico" }; public SessionCreationPolicy getSessions() { return this.sessions; @@ -73,7 +74,7 @@ public class SecurityProperties { private String realm = "Spring"; - private String path = "/**"; + private String[] path = new String[] { "/**" }; private String role = "USER"; @@ -93,12 +94,12 @@ public class SecurityProperties { this.realm = realm; } - public String getPath() { + public String[] getPath() { return this.path; } - public void setPath(String path) { - this.path = path; + public void setPath(String... paths) { + this.path = paths; } public String getRole() { diff --git a/spring-bootstrap-actuator/src/test/java/org/springframework/bootstrap/actuate/autoconfigure/ManagementConfigurationTests.java b/spring-bootstrap-actuator/src/test/java/org/springframework/bootstrap/actuate/autoconfigure/ManagementConfigurationTests.java index c61e9d1ceb..3d60c7a8a9 100644 --- a/spring-bootstrap-actuator/src/test/java/org/springframework/bootstrap/actuate/autoconfigure/ManagementConfigurationTests.java +++ b/spring-bootstrap-actuator/src/test/java/org/springframework/bootstrap/actuate/autoconfigure/ManagementConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.bootstrap.actuate.autoconfigure; +import javax.servlet.Filter; import javax.servlet.Servlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -133,6 +134,14 @@ public class ManagementConfigurationTests { public Dynamic addServlet(String servletName, Servlet servlet) { return Mockito.mock(Dynamic.class); } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + String filterName, Filter filter) { + // TODO: remove this when @ConditionalOnBean works + return Mockito + .mock(javax.servlet.FilterRegistration.Dynamic.class); + } }; for (ServletContextInitializer initializer : initializers) { try { diff --git a/spring-bootstrap-actuator/src/test/java/org/springframework/bootstrap/actuate/properties/SecurityPropertiesTests.java b/spring-bootstrap-actuator/src/test/java/org/springframework/bootstrap/actuate/properties/SecurityPropertiesTests.java new file mode 100644 index 0000000000..d3fbd0fc38 --- /dev/null +++ b/spring-bootstrap-actuator/src/test/java/org/springframework/bootstrap/actuate/properties/SecurityPropertiesTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2013 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.bootstrap.actuate.properties; + +import java.util.Collections; + +import org.junit.Test; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.bootstrap.bind.RelaxedDataBinder; +import org.springframework.core.convert.support.DefaultConversionService; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +/** + * @author Dave Syer + * + */ +public class SecurityPropertiesTests { + + @Test + public void testBindingIgnoredSingleValued() { + SecurityProperties security = new SecurityProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(security, "security"); + binder.bind(new MutablePropertyValues(Collections.singletonMap( + "security.ignored", "/css/**"))); + assertFalse(binder.getBindingResult().hasErrors()); + assertEquals(1, security.getIgnored().length); + } + + @Test + public void testBindingIgnoredMultiValued() { + SecurityProperties security = new SecurityProperties(); + RelaxedDataBinder binder = new RelaxedDataBinder(security, "security"); + binder.setConversionService(new DefaultConversionService()); + binder.bind(new MutablePropertyValues(Collections.singletonMap( + "security.ignored", "/css/**,/images/**"))); + assertFalse(binder.getBindingResult().hasErrors()); + assertEquals(2, security.getIgnored().length); + } + +} diff --git a/spring-bootstrap-cli/src/main/java/org/springframework/bootstrap/cli/compiler/autoconfigure/SpringBootstrapCompilerAutoConfiguration.java b/spring-bootstrap-cli/src/main/java/org/springframework/bootstrap/cli/compiler/autoconfigure/SpringBootstrapCompilerAutoConfiguration.java index 689ce2e341..3ddb15c904 100644 --- a/spring-bootstrap-cli/src/main/java/org/springframework/bootstrap/cli/compiler/autoconfigure/SpringBootstrapCompilerAutoConfiguration.java +++ b/spring-bootstrap-cli/src/main/java/org/springframework/bootstrap/cli/compiler/autoconfigure/SpringBootstrapCompilerAutoConfiguration.java @@ -74,6 +74,7 @@ public class SpringBootstrapCompilerAutoConfiguration extends CompilerAutoConfig "org.springframework.context.annotation.Bean", "org.springframework.context.ApplicationContext", "org.springframework.context.MessageSource", + "org.springframework.core.annotation.Order", "org.springframework.core.io.ResourceLoader", "org.springframework.bootstrap.CommandLineRunner", "org.springframework.bootstrap.context.annotation.EnableAutoConfiguration"); diff --git a/spring-bootstrap-samples/spring-bootstrap-actuator-sample/src/test/java/org/springframework/bootstrap/sample/test/ManagementAddressServiceBootstrapApplicationTests.java b/spring-bootstrap-samples/spring-bootstrap-actuator-sample/src/test/java/org/springframework/bootstrap/sample/test/ManagementAddressServiceBootstrapApplicationTests.java index 4a885bc18c..3df9d32742 100644 --- a/spring-bootstrap-samples/spring-bootstrap-actuator-sample/src/test/java/org/springframework/bootstrap/sample/test/ManagementAddressServiceBootstrapApplicationTests.java +++ b/spring-bootstrap-samples/spring-bootstrap-actuator-sample/src/test/java/org/springframework/bootstrap/sample/test/ManagementAddressServiceBootstrapApplicationTests.java @@ -28,7 +28,6 @@ import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; /** * Integration tests for separate management and main service ports. @@ -83,10 +82,7 @@ public class ManagementAddressServiceBootstrapApplicationTests { @SuppressWarnings("rawtypes") ResponseEntity entity = getRestTemplate().getForEntity( "http://localhost:" + managementPort + "/metrics", Map.class); - assertEquals(HttpStatus.OK, entity.getStatusCode()); - @SuppressWarnings("unchecked") - Map body = entity.getBody(); - assertTrue("Wrong body: " + body, body.containsKey("counter.status.200.root")); + assertEquals(HttpStatus.UNAUTHORIZED, entity.getStatusCode()); } @Test diff --git a/spring-bootstrap-samples/spring-bootstrap-actuator-ui-sample/src/main/java/org/springframework/bootstrap/sample/ui/ActuatorUiBootstrapApplication.java b/spring-bootstrap-samples/spring-bootstrap-actuator-ui-sample/src/main/java/org/springframework/bootstrap/sample/ui/ActuatorUiBootstrapApplication.java index 202995d893..7632f3f7ac 100644 --- a/spring-bootstrap-samples/spring-bootstrap-actuator-ui-sample/src/main/java/org/springframework/bootstrap/sample/ui/ActuatorUiBootstrapApplication.java +++ b/spring-bootstrap-samples/spring-bootstrap-actuator-ui-sample/src/main/java/org/springframework/bootstrap/sample/ui/ActuatorUiBootstrapApplication.java @@ -4,19 +4,14 @@ import java.util.Date; import java.util.Map; import org.springframework.bootstrap.SpringApplication; -import org.springframework.bootstrap.actuate.autoconfigure.ConditionalOnManagementContext; -import org.springframework.bootstrap.actuate.autoconfigure.ManagementAutoConfiguration; -import org.springframework.bootstrap.actuate.autoconfigure.SecurityAutoConfiguration; -import org.springframework.bootstrap.context.annotation.ConditionalOnExpression; +import org.springframework.bootstrap.actuate.properties.SecurityProperties; import org.springframework.bootstrap.context.annotation.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; -@EnableAutoConfiguration(exclude = { SecurityAutoConfiguration.class, - ManagementAutoConfiguration.class }) +@EnableAutoConfiguration @ComponentScan @Controller public class ActuatorUiBootstrapApplication { @@ -33,19 +28,11 @@ public class ActuatorUiBootstrapApplication { SpringApplication.run(ActuatorUiBootstrapApplication.class, args); } - @Configuration - @ConditionalOnExpression("${server.port:8080} != ${management.port:${server.port:8080}}") - @Import(ManagementAutoConfiguration.class) - protected static class ManagementConfiguration { - - } - - @Configuration - @ConditionalOnExpression("${server.port:8080} != ${management.port:${server.port:8080}}") - @ConditionalOnManagementContext - @Import(SecurityAutoConfiguration.class) - protected static class ManagementSecurityConfiguration { - + @Bean + public SecurityProperties securityProperties() { + SecurityProperties security = new SecurityProperties(); + security.getBasic().setPath(""); // empty + return security; } } diff --git a/spring-bootstrap-samples/spring-bootstrap-actuator-ui-sample/src/test/java/org/springframework/bootstrap/sample/ui/ActuatorUiBootstrapApplicationTests.java b/spring-bootstrap-samples/spring-bootstrap-actuator-ui-sample/src/test/java/org/springframework/bootstrap/sample/ui/ActuatorUiBootstrapApplicationTests.java index b6bf1b62a6..a909f3f4c0 100644 --- a/spring-bootstrap-samples/spring-bootstrap-actuator-ui-sample/src/test/java/org/springframework/bootstrap/sample/ui/ActuatorUiBootstrapApplicationTests.java +++ b/spring-bootstrap-samples/spring-bootstrap-actuator-ui-sample/src/test/java/org/springframework/bootstrap/sample/ui/ActuatorUiBootstrapApplicationTests.java @@ -1,6 +1,7 @@ package org.springframework.bootstrap.sample.ui; import java.io.IOException; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -68,6 +69,14 @@ public class ActuatorUiBootstrapApplicationTests { assertTrue("Wrong body:\n" + entity.getBody(), entity.getBody().contains("body")); } + @Test + public void testMetrics() throws Exception { + @SuppressWarnings("rawtypes") + ResponseEntity entity = getRestTemplate().getForEntity( + "http://localhost:8080/metrics", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, entity.getStatusCode()); + } + private RestTemplate getRestTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setErrorHandler(new DefaultResponseErrorHandler() { diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/PropertySourcesBindingPostProcessor.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/PropertySourcesBindingPostProcessor.java index 8e138d2c9a..34a3247b00 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/PropertySourcesBindingPostProcessor.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/annotation/PropertySourcesBindingPostProcessor.java @@ -23,6 +23,7 @@ import org.springframework.bootstrap.bind.PropertiesConfigurationFactory; import org.springframework.bootstrap.context.annotation.EnableConfigurationPropertiesImportSelector.ConfigurationPropertiesHolder; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.PropertySources; import org.springframework.validation.Validator; @@ -40,6 +41,8 @@ public class PropertySourcesBindingPostProcessor implements BeanPostProcessor { private ConversionService conversionService; + private DefaultConversionService defaultConversionService = new DefaultConversionService(); + /** * @param propertySources */ @@ -81,7 +84,10 @@ public class PropertySourcesBindingPostProcessor implements BeanPostProcessor { target); factory.setPropertySources(this.propertySources); factory.setValidator(this.validator); - factory.setConversionService(this.conversionService); + // If no explicit conversion service is provided we add one so that (at least) + // comma-separated arrays of convertibles can be bound automatically + factory.setConversionService(this.conversionService == null ? this.defaultConversionService + : this.conversionService); String targetName = null; if (annotation != null) { factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields()); diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/EnableConfigurationPropertiesTests.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/EnableConfigurationPropertiesTests.java index 8ca7e694c3..ebc395054c 100644 --- a/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/EnableConfigurationPropertiesTests.java +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/context/annotation/EnableConfigurationPropertiesTests.java @@ -48,6 +48,15 @@ public class EnableConfigurationPropertiesTests { assertEquals("foo", this.context.getBean(TestProperties.class).getName()); } + @Test + public void testArrayPropertiesBinding() { + this.context.register(TestConfiguration.class); + TestUtils.addEnviroment(this.context, "name:foo", "array:1,2,3"); + this.context.refresh(); + assertEquals(1, this.context.getBeanNamesForType(TestProperties.class).length); + assertEquals(3, this.context.getBean(TestProperties.class).getArray().length); + } + @Test public void testPropertiesBindingWithoutAnnotation() { this.context.register(MoreConfiguration.class); @@ -186,6 +195,7 @@ public class EnableConfigurationPropertiesTests { @ConfigurationProperties protected static class TestProperties { private String name; + private int[] array; public String getName() { return this.name; @@ -194,6 +204,14 @@ public class EnableConfigurationPropertiesTests { public void setName(String name) { this.name = name; } + + public void setArray(int... values) { + this.array = values; + } + + public int[] getArray() { + return this.array; + } } protected static class MoreProperties {