浏览代码

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 10 年之前
父节点
当前提交
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/`.