Commit 4d608f20 authored by Dave Syer's avatar Dave Syer

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
parent 96e10104
...@@ -202,6 +202,7 @@ public class ManagementSecurityAutoConfiguration { ...@@ -202,6 +202,7 @@ public class ManagementSecurityAutoConfiguration {
@Configuration @Configuration
@ConditionalOnMissingBean(AuthenticationManager.class) @ConditionalOnMissingBean(AuthenticationManager.class)
@Order(Ordered.LOWEST_PRECEDENCE - 4)
protected static class ManagementAuthenticationManagerConfiguration extends protected static class ManagementAuthenticationManagerConfiguration extends
AuthenticationManagerConfiguration { AuthenticationManagerConfiguration {
} }
......
...@@ -18,22 +18,20 @@ package org.springframework.boot.actuate.autoconfigure; ...@@ -18,22 +18,20 @@ package org.springframework.boot.actuate.autoconfigure;
import org.junit.After; import org.junit.After;
import org.junit.Test; 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.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.context.listener.LoggingApplicationListener;
import org.springframework.boot.test.EnvironmentTestUtils; import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.mock.web.MockServletContext; import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 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.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.AuthorityUtils;
...@@ -74,7 +72,7 @@ public class ManagementSecurityAutoConfigurationTests { ...@@ -74,7 +72,7 @@ public class ManagementSecurityAutoConfigurationTests {
ManagementServerPropertiesAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class); PropertyPlaceholderAutoConfiguration.class);
this.context.refresh(); 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 // 6 for static resources, one for management endpoints and one for the rest
assertEquals(8, this.context.getBean(FilterChainProxy.class).getFilterChains() assertEquals(8, this.context.getBean(FilterChainProxy.class).getFilterChains()
.size()); .size());
...@@ -89,9 +87,9 @@ public class ManagementSecurityAutoConfigurationTests { ...@@ -89,9 +87,9 @@ public class ManagementSecurityAutoConfigurationTests {
HttpMessageConvertersAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
ManagementServerPropertiesAutoConfiguration.class, ManagementServerPropertiesAutoConfiguration.class,
SecurityAutoConfiguration.class, SecurityAutoConfiguration.class,
ManagementSecurityAutoConfiguration.class, ManagementSecurityAutoConfiguration.class, UserDetailsExposed.class,
PropertyPlaceholderAutoConfiguration.class); PropertyPlaceholderAutoConfiguration.class);
debugRefresh(this.context); this.context.refresh();
UserDetails user = getUser(); UserDetails user = getUser();
assertTrue(user.getAuthorities().containsAll( assertTrue(user.getAuthorities().containsAll(
AuthorityUtils AuthorityUtils
...@@ -169,17 +167,24 @@ public class ManagementSecurityAutoConfigurationTests { ...@@ -169,17 +167,24 @@ public class ManagementSecurityAutoConfigurationTests {
this.context.getBean(AuthenticationManager.class)); this.context.getBean(AuthenticationManager.class));
} }
private static AnnotationConfigWebApplicationContext debugRefresh( @Configuration
AnnotationConfigWebApplicationContext context) { protected static class UserDetailsExposed implements
EnvironmentTestUtils.addEnvironment(context, "debug:true"); WebSecurityConfigurer<WebSecurity> {
LoggingApplicationListener logging = new LoggingApplicationListener();
logging.onApplicationEvent(new SpringApplicationBeforeRefreshEvent( @Override
new SpringApplication(), context, new String[0])); public void init(WebSecurity builder) throws Exception {
AutoConfigurationReportLoggingInitializer initializer = new AutoConfigurationReportLoggingInitializer(); }
initializer.initialize(context);
context.refresh(); @Override
initializer.onApplicationEvent(new ContextRefreshedEvent(context)); public void configure(WebSecurity builder) throws Exception {
return context; }
@Bean
public AuthenticationManager authenticationManager(
AuthenticationManagerBuilder builder) throws Exception {
return builder.getOrBuild();
}
} }
@Configuration @Configuration
......
...@@ -16,22 +16,20 @@ ...@@ -16,22 +16,20 @@
package org.springframework.boot.autoconfigure.security; package org.springframework.boot.autoconfigure.security;
import java.util.LinkedHashSet;
import java.util.List; 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.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties.User; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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}. * Configuration for a Spring Security in-memory {@link AuthenticationManager}.
...@@ -41,10 +39,10 @@ import org.springframework.security.config.annotation.authentication.configurers ...@@ -41,10 +39,10 @@ import org.springframework.security.config.annotation.authentication.configurers
@Configuration @Configuration
@ConditionalOnBean(ObjectPostProcessor.class) @ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(AuthenticationManager.class) @ConditionalOnMissingBean(AuthenticationManager.class)
public class AuthenticationManagerConfiguration { @ConditionalOnWebApplication
@Order(Ordered.LOWEST_PRECEDENCE - 3)
private static Log logger = LogFactory public class AuthenticationManagerConfiguration implements
.getLog(AuthenticationManagerConfiguration.class); WebSecurityConfigurer<WebSecurity> {
@Autowired @Autowired
private SecurityProperties security; private SecurityProperties security;
...@@ -52,26 +50,17 @@ public class AuthenticationManagerConfiguration { ...@@ -52,26 +50,17 @@ public class AuthenticationManagerConfiguration {
@Autowired @Autowired
private List<SecurityPrequisite> dependencies; private List<SecurityPrequisite> dependencies;
@Bean @Override
public AuthenticationManager authenticationManager( public void init(WebSecurity builder) throws Exception {
ObjectPostProcessor<Object> objectPostProcessor) throws Exception { }
InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> 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<String> roles = new LinkedHashSet<String>(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);
} }
} }
...@@ -16,17 +16,39 @@ ...@@ -16,17 +16,39 @@
package org.springframework.boot.autoconfigure.security; 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.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 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.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationManager; 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 <code>@Configuration</code> with <code>@EnableWebSecurity</code>. To just add
* your own layer of application security in front of the defaults, add a
* <code>@Configuration</code> of type {@link WebSecurityConfigurerAdapter}.
* *
* @author Dave Syer * @author Dave Syer
*/ */
...@@ -37,10 +59,67 @@ import org.springframework.security.authentication.AuthenticationManager; ...@@ -37,10 +59,67 @@ import org.springframework.security.authentication.AuthenticationManager;
AuthenticationManagerConfiguration.class }) AuthenticationManagerConfiguration.class })
public class SecurityAutoConfiguration { public class SecurityAutoConfiguration {
private static Log logger = LogFactory.getLog(SecurityAutoConfiguration.class);
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public SecurityProperties securityProperties() { public SecurityProperties securityProperties() {
return new SecurityProperties(); return new SecurityProperties();
} }
@Bean
@ConditionalOnBean(AuthenticationManagerBuilder.class)
@ConditionalOnMissingBean
public AuthenticationManager authenticationManager(
AuthenticationManagerBuilder builder, ObjectPostProcessor<Object> 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<String> roles = new LinkedHashSet<String>(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));
}
} }
...@@ -34,6 +34,7 @@ import org.springframework.mock.web.MockServletContext; ...@@ -34,6 +34,7 @@ import org.springframework.mock.web.MockServletContext;
import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.TestingAuthenticationToken; 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.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterChainProxy;
...@@ -58,7 +59,7 @@ public class SecurityAutoConfigurationTests { ...@@ -58,7 +59,7 @@ public class SecurityAutoConfigurationTests {
this.context.register(SecurityAutoConfiguration.class, this.context.register(SecurityAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class); PropertyPlaceholderAutoConfiguration.class);
debugRefresh(this.context); debugRefresh(this.context);
assertNotNull(this.context.getBean(AuthenticationManager.class)); assertNotNull(this.context.getBean(AuthenticationManagerBuilder.class));
// 4 for static resources and one for the rest // 4 for static resources and one for the rest
assertEquals(5, this.context.getBean(FilterChainProxy.class).getFilterChains() assertEquals(5, this.context.getBean(FilterChainProxy.class).getFilterChains()
.size()); .size());
......
...@@ -48,7 +48,7 @@ public class SampleSecureApplication implements CommandLineRunner { ...@@ -48,7 +48,7 @@ public class SampleSecureApplication implements CommandLineRunner {
} }
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
SpringApplication.run(SampleSecureApplication.class, args); SpringApplication.run(SampleSecureApplication.class, "--debug");
} }
} }
...@@ -16,12 +16,15 @@ ...@@ -16,12 +16,15 @@
package sample.secure; package sample.secure;
import static org.junit.Assert.assertEquals;
import org.junit.After; import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration; 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.Configuration;
import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.PropertySource;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
...@@ -34,8 +37,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; ...@@ -34,8 +37,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import sample.secure.SampleSecureApplicationTests.TestConfiguration; import sample.secure.SampleSecureApplicationTests.TestConfiguration;
import static org.junit.Assert.assertEquals;
/** /**
* Basic integration tests for demo application. * Basic integration tests for demo application.
* *
...@@ -50,8 +51,17 @@ public class SampleSecureApplicationTests { ...@@ -50,8 +51,17 @@ public class SampleSecureApplicationTests {
private SampleService service; private SampleService service;
@Autowired @Autowired
private ApplicationContext context;
private Authentication authentication; private Authentication authentication;
@Before
public void init() {
AuthenticationManager authenticationManager = context.getBean(AuthenticationManager.class);
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
"user", "password"));
}
@After @After
public void close() { public void close() {
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
...@@ -84,16 +94,6 @@ public class SampleSecureApplicationTests { ...@@ -84,16 +94,6 @@ public class SampleSecureApplicationTests {
@Configuration @Configuration
protected static class TestConfiguration { protected static class TestConfiguration {
@Autowired
private AuthenticationManager authenticationManager;
@Bean
public Authentication user() {
return authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken("user",
"password"));
}
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment