问题
I have spring boot webapp that uses the Java-based configuration to configure a JdbcUserDetailsManager:
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
protected DataSource dataSource;
@Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select username as principal, password as credentials, true from users where username = ?")
.authoritiesByUsernameQuery("select username as principal, authority as role from authorities where username = ?")
.rolePrefix("ROLE_");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated()
.and()
.formLogin()
.successHandler(
(request, response, authentication) -> {
response.setStatus(HttpStatus.NO_CONTENT.value());
})
.failureHandler(
(request, response, authentication) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
})
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(
(request, response, authentication) -> {
response.setStatus(HttpStatus.NO_CONTENT.value());
});
}
}
I can set a breakpoint in configAuthentication()
, so I know that the method is getting called. I now want to get the JdbcUserDetailsManager
injected in my Application class:
@EnableAutoConfiguration
@ComponentScan
public class Application {
private Environment env;
private UserDetailsManager userDetailsManager;
@Autowired
public Application(JdbcTemplate jdbcTemplate, Environment env, UserDetailsManager userDetailsManager) {
this.env = env;
this.userDetailsManager = userDetailsManager;
...
When I try to start my application, I get the following error:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'application': Unsatisfied dependency expressed through constructor argument with index 2 of type [org.springframework.security.provisioning.UserDetailsManager]: : No qualifying bean of type [org.springframework.security.provisioning.UserDetailsManager] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.security.provisioning.UserDetailsManager] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
But I know for a fact that a JdbcUserDetailsManager is getting instantiated before the Application
constructor is called. What's going on here? How can I validate that the JdbcUserDetailsManager is actually registered with the context?
Update: By changing my SecurityConfig
as follows, I was able to resolve the problem:
@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
protected DataSource dataSource;
private JdbcUserDetailsManager userDetailsManager;
@Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
this.userDetailsManager = auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery(
"select username,password,enabled from users where username=?")
.authoritiesByUsernameQuery(
"select username, role from user_roles where username=?").getUserDetailsService();
}
@Bean(name = "userDetailsManager")
public JdbcUserDetailsManager getUserDetailsManager() {
return userDetailsManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated()
.and()
.formLogin()
.successHandler(
(request, response, authentication) -> {
response.setStatus(HttpStatus.NO_CONTENT.value());
})
.failureHandler(
(request, response, authentication) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
})
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(
(request, response, authentication) -> {
response.setStatus(HttpStatus.NO_CONTENT.value());
});
}
}
Heads up to Plínio Pantaleão for pushing me in the right direction. Unfortunately, I cannot award the Bounty for a comment. I'm also still not clear on why the AuthenticationManagerBuilder
does not register the UserDetailsService as a Bean in the context automatically. If anybody can provide an authoritative answer on why I have to provide a getter or can explain how to make it work without the getter (which feels somewhat hacky to me), I will award the bounty for that answer.
回答1:
Spring injects beans, so you have to have a bean on the context for the injection to occur.
But don't create the bean in the configAuthentication()
method. Create it in its own method, and then reference that from the configAuthentication()
method. Like this:
@Bean
public JdbcUserDetailsManager userDetailsManager() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(dataSource);
manager.setUsersByUsernameQuery(
"select username,password,enabled from users where username=?");
manager.setAuthoritiesByUsernameQuery(
"select username, role from user_roles where username=?");
manager.setRolePrefix("ROLE_");
return manager;
}
@Autowired
public void configAuthentication(AuthenticationManagerBuilder builder)
throws Exception {
builder.userDetailsService(userDetailsManager());
}
Now userDetailsManager()
produces a properly-configured bean (allowing injection) and you're using it for authentication. Spring does some magic here to ensure that repeated calls to userDetailsManager()
(or any other bean definition) return the same object over and over, instead of creating new instances every time.
I changed your method name from getUserDetailsManager()
to userDetailsManager()
. This method is a bean definition, not a getter, so that's the reason. Also I removed the name from the @Bean
annotation since Spring automatically uses the method name for the bean name here.
A couple of additional notes to fill in some details:
First, the call to jdbcAuthentication()
results in a new JdbcUserDetailsManager
instance, but it's wholly internal (i.e., not a Spring-managed bean). We can tell because Spring complains when there are multiple beans satisfying a single injection. For details, look at the source code for AuthenticationManagerBuilder, JdbcUserDetailsManagerConfigurer and various superclasses. Basically what you will see is that the jdbcAuthentication()
call results in an internal details manager, which the call to userDetailsService()
replaces.
Second, calling userDetailsService()
discards the jdbcAuthentication()
configuration. Here's the relevant method from AuthenticationManagerBuilder
:
public <T extends UserDetailsService>
DaoAuthenticationConfigurer<AuthenticationManagerBuilder,T>
userDetailsService(T userDetailsService) throws Exception {
this.defaultUserDetailsService = userDetailsService;
return apply(
new DaoAuthenticationConfigurer<AuthenticationManagerBuilder,T>
(userDetailsService));
}
That's why we've moved the JdbcUserDetailsManager
configuration out of the jdbcAuthentication()
piece and into the userDetailsManager()
method itself. (The jdbcAuthentication()
call basically exposes a convenient, fluent interface for creating the JdbcUserDetailsManager
, but we don't need it here because we already have our JdbcUserDetailsManager
.)
回答2:
There is (now?) a better way to to this by overriding WebSecurityConfigurerAdapter.userDetailsServiceBean()
and registering it as @Bean
:
public static class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean(name = "myUserDetailsService")
// any or no name specified is allowed
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
}
Javadoc:
Override this method to expose a UserDetailsService created from configure(AuthenticationManagerBuilder) as a bean. In general only the following override should be done of this method:
see above example
To change the instance returned, developers should change userDetailsService() instead
This method is also mentioned in the Javadoc of WebSecurityConfigurerAdapter.configure(AuthenticationManagerBuilder)
来源:https://stackoverflow.com/questions/25631791/cannot-get-userdetailsmanager-injected-with-spring-boot-and-java-based-configura