0

How can I define my custom UserDetailsService bean in a way that enables my spring mvc web app to use my underlying MySQL database to check authentication for users and passwords?

Here are the specifics:

I am adding security to the spring petclinic sample as a way of learning about spring security. I am using Java configuration, and have set up a SecurityConfig.java file which extends WebSecurityConfigurerAdapter. I am trying to set up JdbcAuthentication in a way that utilizes a MySQL database managed by the ClinicService tools that are built into the petclinic sample. I therefore created a CustomUserDetailsService class which extends UserDetailsService, and which is intented to link the SecurityConfig.java with ClinicService.java. I created a User class and a Role class to model the users and roles tables in the MySQL database, respectively.

I then added the following line to business-config.xml to define the CustomUserDetailService:

<bean class="org.springframework.samples.petclinic.service.CustomUserDetailsService"></bean>

But yet I am still getting the following error stating that the bean for CustomUserDetailService has not been defined:

Caused by: java.lang.IllegalArgumentException: Can not set  
org.springframework.samples.petclinic.service.CustomUserDetailsService field  
org.springframework.security.samples.petclinic.config.SecurityConfig.myCustomUserDetailsService  
to $Proxy61

To keep this posting concise, I have loaded the relevant backup materials to a file sharing site. You can read all the source code and the complete stack trace by clicking on the following links:

You can read SecurityConfig.java by clicking on this link.
The code for business-config.xml is at this link.
The code for CustomUserDetailService.java is at this link.
The code for the User entity is at this link.
The code for the Role entity is at this link.
The complete stack trace can be read at this link.

All of the other code for the application can be found at the github page for the Spring petclinic sample, which you can read by clicking on this link.

Here is a link to the code for login.jsp.

Here is a link to my revised business-config.xml code.

CodeMed
  • 9,527
  • 70
  • 212
  • 364
  • It seems your java config style configuration doesn't detect a bean defined in xml config. Take a look at http://stackoverflow.com/questions/13254779/how-to-import-java-config-class-into-xml-config-so-that-both-contexts-have-beans and try adding `` to xml config and see if it helps. – Krešimir Nesek Apr 01 '14 at 15:06
  • I would add it under first `` in your business-config.xml – Krešimir Nesek Apr 01 '14 at 15:36
  • You should specify an interface as a dependency in SecurityConfig and let spring container autowire an implementation - change CustomUserDetailsService to UserDetailsService. Also, you don't need jdbc authentication if you intend to use your custom UserDetailsService to perform authentication and authorisation (e.g. throw UsernameNotFoundException for your UserDetailsService if user doesn't exist). – Krešimir Nesek Apr 01 '14 at 16:09

1 Answers1

4

To summarize comments, here's the answer.

There are several things wrong here:

1) When mixing XML configuration and Java configuration in Spring in a way that java config is imported inside xml config file, <context:annotation-config/> needs to be present in xml file and java config class needs to be declared as a bean. <context:annotation-config/> will enable annotation processing of the declared bean and @Configuration annotation will then be processed. Read more at the documentation: http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/beans.html#beans-java-combining

To fix te problem insert <context:annotation-config/> in business-config.xml. <context:annotation-config/> is needed for <bean class="org.springframework.security.samples.petclinic.config.SecurityConfig"></bean> to work so they need to be declared in the same bean profile.

2) You are autowiring a concrete class (CustomUserDetailsSerivce) in SpringConfig instead of an interface (UserDetailsService). While it's possible to use Spring to autowire concrete classes usually it's better to autowire to interfaces (Spring will autowire concreate CustomUserDetailsSerivce implementation to @Autowired UserDetailsService field). Spring creates proxies around wired classes to enable certain features (such as declarative transactions) and such proxies may easily implement an interface when autowiring but may fail if attempted to be autowired to a concrete class. It is possible though to achieve it - more information here: Spring Autowiring class vs. interface? In this case, it's definitely better to autowire to UserDetailsService interface since that is what our security config actually depends on.

To fix this issue specify field type as UserDetailsService in SpringConfig:

//Use UseDetailsService interface as field type instead of concrete class CustomUserDao
@Autowired
private UserDetailsService myCustomUserDetailsService;

3) You seem to be setting up both jdbc authentication and authentication using a custom user details service. Spring JDBC authentication is usually used if you want Spring Security to use jdbc to query a database and find about existing users and their roles etc... Custom UserDetailsSerivce is used if you want implement querying for users/roles etc... your self. In your example (since you are providing your custom UserDetailsService which will query the backend using ClinicService) you don't need JDBC authentication.

Here's an example of working spring security configuration that uses custom UserDetailsService (implemented elsewhere and autowired by spring) via java config:

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/petclinic/")
                .usernameParameter("j_username") // default is username
                .passwordParameter("j_password") // default is password
                .loginProcessingUrl("/j_spring_security_check") // default is /login with an HTTP post 
                .failureUrl("/login")
                .permitAll()
                .and()
            .logout()
                .logoutSuccessUrl("/index.jsp")
                .and()
            .authorizeRequests()
                .antMatchers("/**").hasRole("ROLE_ADMIN")
                .antMatchers("/j_spring_security_check").permitAll()
                .and()
            .userDetailsService(userDetailsService);
    }
}

I suggest reading the actual documentation as it describes what specific configuration builder methods do and provide the examples: http://docs.spring.io/spring-security/site/docs/3.2.0.RC2/apidocs/org/springframework/security/config/annotation/web/builders/HttpSecurity.html#formLogin()

EDIT 1 - added login form configuration and link to documentation

EDIT 2 - added more explanations for problem 1)

EDIT 3 - changed role name form "ADMIN" to "ROLE_ADMiN" to match role names in UserDetailsService

Community
  • 1
  • 1
Krešimir Nesek
  • 5,302
  • 4
  • 29
  • 56
  • Added additional explanations and code samples to the answers. – Krešimir Nesek Apr 01 '14 at 18:16
  • Login form looks good. To debug I'd setup debugger breakpoint in loadUserByUsername and see if it executes on login attempt and verify that UserDetails that you return contain values you expect. Also let's permit access to login processing url to everyone (see edited answer for addtional antMatcher. – Krešimir Nesek Apr 01 '14 at 19:10
  • @CodeMed If you got rid of the exception and your `UserDetailService` gets properly injected your question is answered. All the rest is bonus. I don't think it's fair to push someone into fixing all possible issues with your project until you can accept his answer. If you have issues don't really in printing text but do some actual debugging and set breakpoints at vital locations and have a look at the environment. – Bart Apr 01 '14 at 19:27
  • I believe UserDetailsService is not called because form submits to wrong url (it seems old xml config default changed when using java config). Edited answer with configuration that matches the login form. Also agree this question should actually be 4 questions. – Krešimir Nesek Apr 01 '14 at 19:48
  • No need to +1 other answers. Since we've got this far lets finish it; What error do you get? Are there any exceptions in log? To debug verify getAuthorities(domainUser.getRole().getId()) returns what you expect. Also you're setting up authority names as "ROLE_ADMIN" in UserDetailsService. Try to change that to "ADMIN" without ROLE_ prefix. – Krešimir Nesek Apr 01 '14 at 20:52
  • @KresimirNesek It works now. +1 and credit for the answer. I have given you +85 today and will login later to find a couple other of your answers which, independently and on their own merits, also are worth +1. I just want to wait to do that so it does not set off any trigger. All I am doing is paying attention to answers which independently deserve their own kudos, so I don't think there is anything wrong with it. Thank you again for your patience with me. You helped me a lot. – CodeMed Apr 01 '14 at 20:55