Silly Freak
Silly Freak

Reputation: 4231

How to make a UserDetailsManager available as a bean

I'm experimenting with spring-security for authentication and want to be able to add users at runtime. I figured using UserDetailsManager would be minimally intrusive. How do I make it available as a bean, so that I can access it in controllers and other objects?

The Code I was starting with is as follows:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, DataSource dataSource, PasswordEncoder enc) throws Exception {
    auth.jdbcAuthentication().dataSource(dataSource).withDefaultSchema().passwordEncoder(enc)
            .withUser("user").password(enc.encode("password")).roles("USER").and()
            .withUser("admin").password(enc.encode("password")).roles("USER", "ADMIN");
}

jdbcAuthentication() creates a JdbcUserDetailsManager, everything works fine. But I don't know to how to access that after the web app's initialization. I tried two variants that didn't work:

@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource,PasswordEncoder enc) throws Exception {
    return new JdbcUserDetailsManagerConfigurer<>().dataSource(dataSource).withDefaultSchema().passwordEncoder(enc)
            .withUser("user").password(enc.encode("password")).roles("USER").and()
            .withUser("admin").password(enc.encode("password")).roles("USER", "ADMIN").and()
            .getUserDetailsService();
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsManager userDetailsManager) throws Exception {
    auth.userDetailsService(userDetailsManager);
}

When filling out the login form, I get the following:

Table "USERS" not found; SQL statement:
select username,password,enabled from users where username = ? [42102-185]

So it seems that this does not initialize the bean properly. Second try:

@Bean
public UserDetailsManager userDetailsManager(AuthenticationManagerBuilder auth, DataSource dataSource, PasswordEncoder enc) throws Exception {
    return auth.jdbcAuthentication().dataSource(dataSource).withDefaultSchema().passwordEncoder(enc)
            .withUser("user").password(enc.encode("password")).roles("USER").and()
            .withUser("admin").password(enc.encode("password")).roles("USER", "ADMIN").and()
            .getUserDetailsService();
}

During initialization, I get:

java.lang.IllegalStateException: Cannot apply ...JdbcUserDetailsManagerConfigurer@3bd97b0d to already built object

So using the builder in an @Bean method does not work either.

Upvotes: 2

Views: 3816

Answers (2)

Silly Freak
Silly Freak

Reputation: 4231

jdbcAuthentication() does more than just creating a JdbcUserDetailsManagerConfigurer; most importantly, it registers the configurer using apply(), so that all configurations can later be executed in the right order.

Luckily, apply() is public, so we can call it ourselves. Knowing that, we can move the main configuration load back into configureGlobal, using the configurer's alternate constructor. What worked for me in the end is the following:

@Bean
public JdbcUserDetailsManager userDetailsManager(DataSource dataSource) {
    JdbcUserDetailsManager mgr = new JdbcUserDetailsManager();
    mgr.setDataSource(dataSource); // (1)
    return mgr;
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, JdbcUserDetailsManager userDetailsManager, DataSource dataSource, PasswordEncoder enc) throws Exception {
    //set user detail service manually
    auth.userDetailsService(userDetailsManager);
    JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> conf =
            new JdbcUserDetailsManagerConfigurer<>(userDetailsManager);
    //apply the configurer
    auth.apply(conf);
    conf.dataSource(dataSource) // (2)
            .withDefaultSchema().passwordEncoder(enc)
            .withUser("user").password(enc.encode("password")).roles("USER").and()
            .withUser("admin").password(enc.encode("password")).roles("USER", "ADMIN");
}

It seems that setting the data source in (1) and (2) is redundant, but leaving either one out results in an exception during initialization:

//without (1)
java.lang.IllegalArgumentException: 'dataSource' or 'jdbcTemplate' is required
//without (2)
java.lang.IllegalStateException: DataSource must be set

The first error is the validation of the bean returned by userDetailsManager(), the second that of the configurer before it is executed by the AuthenticationManagerBuilder.

Configured like this, I can write, e.g. in a controller:

@Autowired
private UserDetailsManager users;
@Autowired
private PasswordEncoder enc;

@RequestMapping(...)
public String handle(Model model) {
    users.createUser(new User(username, enc.encode(password), authorities);

    return "view";
}

Upvotes: 3

codemania23
codemania23

Reputation: 1091

If i got you right, you want to create a bean that handles users, i would suggest declaring it in the spring.xml and using functions that keep adding to the bean. try using an applicationcontext, this will be on main page

ApplicationContext ac;
        ac=new ClassPathXmlApplicationContext("Spring.xml");
        prisontest par=(prisontest)ac.getBean("prison");

here, applicationcontext is like a container holding beans and prisontest is class that defines the datasource, u can understand seeing the spring.xml,

the spring.xml will look in this way:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean id="prison" class="prisontest">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource">

        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/tgmc" />
        <property name="username" value="root" />
        <property name="password" value="root" />
    </bean>

</beans>

and the bean that refers to the datasource will be like this:

public class prisontest {
        DataSource ds;
        public void setDataSource(DataSource ds)
        {
            this.ds=ds;
        }

add your code here for managing users on runtime }

you can also put your other code in the same class, hope this helps.

Upvotes: 0

Related Questions