Procházet zdrojové kódy

SEC-2868: Simplify custom UserDetailsService Java Config

Exposing a UserDetailsService as a bean is now all that is necessary
for Java based configuration. Additionally, an optional PasswordEncoder
bean can be used to configure password encoding.
Rob Winch před 10 roky
rodič
revize
bac980cbcb

+ 5 - 0
config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java

@@ -68,6 +68,11 @@ public class AuthenticationConfiguration {
 		return new EnableGlobalAuthenticationAutowiredConfigurer(context);
 	}
 
+	@Bean
+	public static InitializeUserDetailsBeanManagerConfigurer initializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
+		return new InitializeUserDetailsBeanManagerConfigurer(context);
+	}
+
 	public AuthenticationManager getAuthenticationManager() throws Exception {
 		if (authenticationManagerInitialized) {
 			return authenticationManager;

+ 88 - 0
config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java

@@ -0,0 +1,88 @@
+/*
+ * 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.security.config.annotation.authentication.configuration;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+/**
+ * Lazily initializes the global authentication with a
+ * {@link UserDetailsService} if it is not yet configured and there is only a
+ * single Bean of that type. Optionally, if a {@link PasswordEncoder} is defined
+ * will wire this up too.
+ *
+ * @author Rob Winch
+ * @since 4.1
+ */
+@Order(Ordered.LOWEST_PRECEDENCE - 5000)
+class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {
+
+	private final ApplicationContext context;
+
+	/**
+	 * @param context
+	 */
+	public InitializeUserDetailsBeanManagerConfigurer(ApplicationContext context) {
+		this.context = context;
+	}
+
+	@Override
+	public void init(AuthenticationManagerBuilder auth) throws Exception {
+		auth.apply(new InitializeUserDetailsManagerConfigurer());
+	}
+
+	class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {
+		@Override
+		public void configure(AuthenticationManagerBuilder auth) throws Exception {
+			if (auth.isConfigured()) {
+				return;
+			}
+			UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class);
+			if(userDetailsService == null) {
+				return;
+			}
+
+			PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
+
+			DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+			provider.setUserDetailsService(userDetailsService);
+			if(passwordEncoder != null) {
+				provider.setPasswordEncoder(passwordEncoder);
+			}
+
+			auth
+				.authenticationProvider(provider);
+		}
+
+		/**
+		 * @return
+		 */
+		private <T> T getBeanOrNull(Class<T> type) {
+			String[] userDetailsBeanNames = context.getBeanNamesForType(type);
+			if(userDetailsBeanNames.length != 1) {
+				return null;
+			}
+
+			return context.getBean(userDetailsBeanNames[0], type);
+		}
+	}
+}

+ 64 - 7
config/src/test/groovy/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.groovy

@@ -16,13 +16,7 @@
 package org.springframework.security.config.annotation.authentication.configuration;
 
 import org.springframework.aop.framework.ProxyFactoryBean
-import org.springframework.beans.BeansException
 import org.springframework.beans.factory.annotation.Autowired
-import org.springframework.beans.factory.config.BeanPostProcessor
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
-import org.springframework.beans.factory.support.BeanDefinitionRegistry
-import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
-import org.springframework.context.ApplicationContext
 import org.springframework.context.annotation.Bean
 import org.springframework.context.annotation.Configuration
 import org.springframework.context.annotation.Import
@@ -41,11 +35,13 @@ import org.springframework.security.config.annotation.configuration.ObjectPostPr
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
-import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity
 import org.springframework.security.core.AuthenticationException
 import org.springframework.security.core.authority.AuthorityUtils
 import org.springframework.security.core.context.SecurityContextHolder
 import org.springframework.security.core.userdetails.User
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.crypto.password.PasswordEncoder
 import org.springframework.security.provisioning.InMemoryUserDetailsManager
 
 class AuthenticationConfigurationTests extends BaseSpringSpec {
@@ -366,4 +362,65 @@ class AuthenticationConfigurationTests extends BaseSpringSpec {
 
 		static class BootGlobalAuthenticationConfigurerAdapter extends GlobalAuthenticationConfigurerAdapter { }
 	}
+
+	def 'SEC-2868: Allow Configure UserDetailsService'() {
+		setup:
+		UserDetailsService uds = Mock()
+		UserDetailsServiceBeanConfig.UDS = uds
+		loadConfig(UserDetailsServiceBeanConfig)
+		AuthenticationManager am = context.getBean(AuthenticationConfiguration).getAuthenticationManager()
+		when:
+		am.authenticate(new UsernamePasswordAuthenticationToken("user", "password"))
+		then:
+		1 * uds.loadUserByUsername("user") >> new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER"))
+		when:
+		am.authenticate(new UsernamePasswordAuthenticationToken("user", "invalid"))
+		then:
+		1 * uds.loadUserByUsername("user") >> new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER"))
+		thrown(AuthenticationException.class)
+	}
+
+	@Configuration
+	@Import([AuthenticationConfiguration, ObjectPostProcessorConfiguration])
+	static class UserDetailsServiceBeanConfig {
+		static UserDetailsService UDS
+
+		@Bean
+		UserDetailsService userDetailsService() {
+			UDS
+		}
+	}
+
+	def 'SEC-2868: Allow Configure UserDetailsService with PasswordEncoder'() {
+		setup:
+		UserDetailsService uds = Mock()
+		UserDetailsServiceBeanWithPasswordEncoderConfig.UDS = uds
+		loadConfig(UserDetailsServiceBeanWithPasswordEncoderConfig)
+		AuthenticationManager am = context.getBean(AuthenticationConfiguration).getAuthenticationManager()
+		when:
+		am.authenticate(new UsernamePasswordAuthenticationToken("user", "password"))
+		then:
+		1 * uds.loadUserByUsername("user") >> new User("user",'$2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u',AuthorityUtils.createAuthorityList("ROLE_USER"))
+		when:
+		am.authenticate(new UsernamePasswordAuthenticationToken("user", "invalid"))
+		then:
+		1 * uds.loadUserByUsername("user") >> new User("user",'$2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u',AuthorityUtils.createAuthorityList("ROLE_USER"))
+		thrown(AuthenticationException.class)
+	}
+
+	@Configuration
+	@Import([AuthenticationConfiguration, ObjectPostProcessorConfiguration])
+	static class UserDetailsServiceBeanWithPasswordEncoderConfig {
+		static UserDetailsService UDS
+
+		@Bean
+		UserDetailsService userDetailsService() {
+			UDS
+		}
+
+		@Bean
+		PasswordEncoder passwordEncoder() {
+			new BCryptPasswordEncoder()
+		}
+	}
 }

+ 28 - 0
docs/manual/src/docs/asciidoc/index.adoc

@@ -374,6 +374,7 @@ This will give you access to the entire project history (including all releases
 ** <<method-security-meta-annotations,Method Security Meta Annotations>>
 * <<el-access-web-path-variables,Path Variables in Web Security Expressions>>
 * <<test-method-withanonymoususer,@WithAnonymousUser>>
+* <<jc-authentication-userdetailsservice,Simplified UserDetailsService Java Configuration>>
 
 === What's new in Spring Security 4.0
 
@@ -912,6 +913,33 @@ cn: admin
 uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
 ----
 
+[[jc-authentication-userdetailsservice]]
+==== UserDetailsService
+
+You can define custom authentication by exposing a custom `UserDetailsService` as a bean.
+For example, the following will customize authentication assuming that `SpringDataUserDetailsService` implements `UserDetailsService`:
+
+[source,java]
+----
+@Bean
+public SpringDataUserDetailsService springDataUserDetailsService() {
+    return new SpringDataUserDetailsService();
+}
+----
+
+You can also customize how passwords are encoded by exposing a `PasswordEncoder` as a bean.
+For example, if you use bcrypt you can add a bean definition as shown below:
+
+[source,java]
+----
+@Bean
+public BCryptPasswordEncoder passwordEncoder() {
+    return new BCryptPasswordEncoder();
+}
+----
+
+==== LDAP Authentication
+
 === Multiple HttpSecurity
 
 We can configure multiple HttpSecurity instances just as we can have multiple `<http>` blocks. The key is to extend the `WebSecurityConfigurationAdapter` multiple times. For example, the following is an example of having a different configuration for URL's that start with `/api/`.