From 4d608f20e9e966bd1d7848cd2ccb245787eaa1a1 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Wed, 29 Jan 2014 12:32:09 +0000 Subject: [PATCH] Support for AuthenticationManagerBuilder injection into user code Spring Boot provides a default AuthenticatiomManager for getting started quickly with security and never exposing insecure endpoints. To override that feature as users move to the next stage in their project, they may have to do something slightly different depending on whether it is a webapp or not. In any app (web or not), providing a @Bean of type AuthenticationManager always works, but you don't get the benefit of the builder features. In a webapp the user can also extend WebSecurityConfigurerAdapter to provides a custom AuthenticationManager, and the preferred way of doing that is via a void method that is autowired with an AuthenticationManagerBuilder. The default AuthenticationManager is built in a configurer with @Order(LOWEST_PRECEDENCE - 3) so to override it the user's confugrer must have higher precedence (lower @Order). @EnableGlobalMethodSecurity can also be used in a non-webapp, and Spring Boot will still provide a default AuthenticationManager. To override it the user has to either extend GlobalMethodSecurityConfiguration or provide a @Bean of type AuthenticationManager (there's no other way to capture the AuthenticationManagerBuilder that doesn't happen too late in the beans lifecyle). Fixes gh-244 --- .../ManagementSecurityAutoConfiguration.java | 1 + ...agementSecurityAutoConfigurationTests.java | 43 +++++----- .../AuthenticationManagerConfiguration.java | 47 +++++------ .../security/SecurityAutoConfiguration.java | 81 ++++++++++++++++++- .../SecurityAutoConfigurationTests.java | 3 +- .../secure/SampleSecureApplication.java | 2 +- .../secure/SampleSecureApplicationTests.java | 26 +++--- 7 files changed, 139 insertions(+), 64 deletions(-) diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java index 8ac09da81e9..17d59cc5d1c 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfiguration.java @@ -202,6 +202,7 @@ public class ManagementSecurityAutoConfiguration { @Configuration @ConditionalOnMissingBean(AuthenticationManager.class) + @Order(Ordered.LOWEST_PRECEDENCE - 4) protected static class ManagementAuthenticationManagerConfiguration extends AuthenticationManagerConfiguration { } diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java index 3b7b73b2777..bfd02fc9528 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/ManagementSecurityAutoConfigurationTests.java @@ -18,22 +18,20 @@ package org.springframework.boot.actuate.autoconfigure; import org.junit.After; import org.junit.Test; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.SpringApplicationBeforeRefreshEvent; -import org.springframework.boot.autoconfigure.AutoConfigurationReportLoggingInitializer; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; -import org.springframework.boot.context.listener.LoggingApplicationListener; import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.mock.web.MockServletContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; @@ -74,7 +72,7 @@ public class ManagementSecurityAutoConfigurationTests { ManagementServerPropertiesAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); this.context.refresh(); - assertNotNull(this.context.getBean(AuthenticationManager.class)); + assertNotNull(this.context.getBean(AuthenticationManagerBuilder.class)); // 6 for static resources, one for management endpoints and one for the rest assertEquals(8, this.context.getBean(FilterChainProxy.class).getFilterChains() .size()); @@ -89,9 +87,9 @@ public class ManagementSecurityAutoConfigurationTests { HttpMessageConvertersAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class, SecurityAutoConfiguration.class, - ManagementSecurityAutoConfiguration.class, + ManagementSecurityAutoConfiguration.class, UserDetailsExposed.class, PropertyPlaceholderAutoConfiguration.class); - debugRefresh(this.context); + this.context.refresh(); UserDetails user = getUser(); assertTrue(user.getAuthorities().containsAll( AuthorityUtils @@ -169,17 +167,24 @@ public class ManagementSecurityAutoConfigurationTests { this.context.getBean(AuthenticationManager.class)); } - private static AnnotationConfigWebApplicationContext debugRefresh( - AnnotationConfigWebApplicationContext context) { - EnvironmentTestUtils.addEnvironment(context, "debug:true"); - LoggingApplicationListener logging = new LoggingApplicationListener(); - logging.onApplicationEvent(new SpringApplicationBeforeRefreshEvent( - new SpringApplication(), context, new String[0])); - AutoConfigurationReportLoggingInitializer initializer = new AutoConfigurationReportLoggingInitializer(); - initializer.initialize(context); - context.refresh(); - initializer.onApplicationEvent(new ContextRefreshedEvent(context)); - return context; + @Configuration + protected static class UserDetailsExposed implements + WebSecurityConfigurer { + + @Override + public void init(WebSecurity builder) throws Exception { + } + + @Override + public void configure(WebSecurity builder) throws Exception { + } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationManagerBuilder builder) throws Exception { + return builder.getOrBuild(); + } + } @Configuration diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java index a8a5f0e849b..bae272a5000 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/AuthenticationManagerConfiguration.java @@ -16,22 +16,20 @@ package org.springframework.boot.autoconfigure.security; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Set; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.security.SecurityProperties.User; -import org.springframework.context.annotation.Bean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; +import org.springframework.security.config.annotation.web.WebSecurityConfigurer; +import org.springframework.security.config.annotation.web.builders.WebSecurity; /** * Configuration for a Spring Security in-memory {@link AuthenticationManager}. @@ -41,10 +39,10 @@ import org.springframework.security.config.annotation.authentication.configurers @Configuration @ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnMissingBean(AuthenticationManager.class) -public class AuthenticationManagerConfiguration { - - private static Log logger = LogFactory - .getLog(AuthenticationManagerConfiguration.class); +@ConditionalOnWebApplication +@Order(Ordered.LOWEST_PRECEDENCE - 3) +public class AuthenticationManagerConfiguration implements + WebSecurityConfigurer { @Autowired private SecurityProperties security; @@ -52,26 +50,17 @@ public class AuthenticationManagerConfiguration { @Autowired private List dependencies; - @Bean - public AuthenticationManager authenticationManager( - ObjectPostProcessor objectPostProcessor) throws Exception { + @Override + public void init(WebSecurity builder) throws Exception { + } - InMemoryUserDetailsManagerConfigurer builder = new AuthenticationManagerBuilder( - objectPostProcessor).inMemoryAuthentication(); - User user = this.security.getUser(); - - if (user.isDefaultPassword()) { - logger.info("\n\nUsing default password for application endpoints: " - + user.getPassword() + "\n\n"); - } - - Set roles = new LinkedHashSet(user.getRole()); - - builder.withUser(user.getName()).password(user.getPassword()) - .roles(roles.toArray(new String[roles.size()])); - - return builder.and().build(); + @Override + public void configure(WebSecurity builder) throws Exception { + } + @Autowired + public void authentication(AuthenticationManagerBuilder builder) throws Exception { + SecurityAutoConfiguration.authentication(builder, this.security); } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java index 254ef2427e5..db9d3dabe0f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfiguration.java @@ -16,17 +16,39 @@ package org.springframework.boot.autoconfigure.security; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.SecurityProperties.User; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; +import org.springframework.security.config.annotation.ObjectPostProcessor; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.util.ReflectionUtils; /** - * {@link EnableAutoConfiguration Auto-configuration} for Spring Security. + * {@link EnableAutoConfiguration Auto-configuration} for Spring Security. Provides an + * {@link AuthenticationManager} based on configuration bound to a + * {@link SecurityProperties} bean. There is one user (named "user") whose password is + * random and printed on the console at INFO level during startup. In a webapp this + * configuration also secures all web endpoints (except some well-known static resource) + * locations with HTTP basic security. To replace all the default behaviour in a webapp + * provide a @Configuration with @EnableWebSecurity. To just add + * your own layer of application security in front of the defaults, add a + * @Configuration of type {@link WebSecurityConfigurerAdapter}. * * @author Dave Syer */ @@ -37,10 +59,67 @@ import org.springframework.security.authentication.AuthenticationManager; AuthenticationManagerConfiguration.class }) public class SecurityAutoConfiguration { + private static Log logger = LogFactory.getLog(SecurityAutoConfiguration.class); + @Bean @ConditionalOnMissingBean public SecurityProperties securityProperties() { return new SecurityProperties(); } + @Bean + @ConditionalOnBean(AuthenticationManagerBuilder.class) + @ConditionalOnMissingBean + public AuthenticationManager authenticationManager( + AuthenticationManagerBuilder builder, ObjectPostProcessor processor) + throws Exception { + if (!isBuilt(builder)) { + authentication(builder, securityProperties()); + } + else if (builder.getOrBuild() == null) { + builder = new AuthenticationManagerBuilder(processor); + authentication(builder, securityProperties()); + } + return builder.getOrBuild(); + } + + /** + * Convenience method for building the default AuthenticationManager from + * SecurityProperties. + * + * @param builder the AuthenticationManagerBuilder to use + * @param security the SecurityProperties in use + */ + public static void authentication(AuthenticationManagerBuilder builder, + SecurityProperties security) throws Exception { + + if (isBuilt(builder)) { + return; + } + + User user = security.getUser(); + + if (user.isDefaultPassword()) { + logger.info("\n\nUsing default password for application endpoints: " + + user.getPassword() + "\n\n"); + } + + Set roles = new LinkedHashSet(user.getRole()); + + builder.inMemoryAuthentication().withUser(user.getName()) + .password(user.getPassword()) + .roles(roles.toArray(new String[roles.size()])); + + } + + private static boolean isBuilt(AuthenticationManagerBuilder builder) { + Method configurers = ReflectionUtils.findMethod( + AbstractConfiguredSecurityBuilder.class, "getConfigurers"); + Method unbuilt = ReflectionUtils.findMethod( + AbstractConfiguredSecurityBuilder.class, "isUnbuilt"); + ReflectionUtils.makeAccessible(configurers); + ReflectionUtils.makeAccessible(unbuilt); + return !((Collection) ReflectionUtils.invokeMethod(configurers, builder)) + .isEmpty() || !((Boolean) ReflectionUtils.invokeMethod(unbuilt, builder)); + } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java index 6b42ab88724..c9631f51702 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/SecurityAutoConfigurationTests.java @@ -34,6 +34,7 @@ import org.springframework.mock.web.MockServletContext; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.FilterChainProxy; @@ -58,7 +59,7 @@ public class SecurityAutoConfigurationTests { this.context.register(SecurityAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class); debugRefresh(this.context); - assertNotNull(this.context.getBean(AuthenticationManager.class)); + assertNotNull(this.context.getBean(AuthenticationManagerBuilder.class)); // 4 for static resources and one for the rest assertEquals(5, this.context.getBean(FilterChainProxy.class).getFilterChains() .size()); diff --git a/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java b/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java index 3b3519dbec3..ae483343d2a 100644 --- a/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java +++ b/spring-boot-samples/spring-boot-sample-secure/src/main/java/sample/secure/SampleSecureApplication.java @@ -48,7 +48,7 @@ public class SampleSecureApplication implements CommandLineRunner { } public static void main(String[] args) throws Exception { - SpringApplication.run(SampleSecureApplication.class, args); + SpringApplication.run(SampleSecureApplication.class, "--debug"); } } diff --git a/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java b/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java index e029b91fa11..142bb128c3a 100644 --- a/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java +++ b/spring-boot-samples/spring-boot-sample-secure/src/test/java/sample/secure/SampleSecureApplicationTests.java @@ -16,12 +16,15 @@ package sample.secure; +import static org.junit.Assert.assertEquals; + import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.security.access.AccessDeniedException; @@ -34,8 +37,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import sample.secure.SampleSecureApplicationTests.TestConfiguration; -import static org.junit.Assert.assertEquals; - /** * Basic integration tests for demo application. * @@ -50,8 +51,17 @@ public class SampleSecureApplicationTests { private SampleService service; @Autowired + private ApplicationContext context; + private Authentication authentication; + @Before + public void init() { + AuthenticationManager authenticationManager = context.getBean(AuthenticationManager.class); + authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( + "user", "password")); + } + @After public void close() { SecurityContextHolder.clearContext(); @@ -84,16 +94,6 @@ public class SampleSecureApplicationTests { @Configuration protected static class TestConfiguration { - @Autowired - private AuthenticationManager authenticationManager; - - @Bean - public Authentication user() { - return authenticationManager - .authenticate(new UsernamePasswordAuthenticationToken("user", - "password")); - } - } }