Spring Security UserDetailsService

In this article, we will learn about Spring security UserDetailsService. We will learn how to create a custom database-backed UserDetailsService for authentication with Spring Security.

Introduction

If we are using Spring security in our application for the authentication and authorization, you might know UserDetailsService interface. The UserDetailsService is a core interface in Spring Security framework, which is used to retrieve the user’s authentication and authorization information.

This interface has only one method named loadUserByUsername() which we can implement to feed the customer information to the Spring security API. The DaoAuthenticationProvider will use this information to load the user information during authentication process. Here is the definition of the interface.

UserDetails loadUserByUsername(java.lang.String username) throws UsernameNotFoundException

Let’s inspect some important aspects of the interface and its method

  1. Spring security does not use this method directly at any place.
  2. It will only store user information encapsulated into Authentication objects.

Let’s check how to define a custom Spring security UserDetailsService for our application.

1. Application Setup

Let’s start by creating the web application. We can use the IDE or Spring Initializr to bootstrap our application. We are using Spring Initializr for this post as it offer a fast way to pull the dependencies to build our application.

  • Go to https://start.spring.io/.
  • Select the web, Spring security, Thymeleaf and MySQL as dependencies.
  • Fill information for the group and artifact and click on the “Generate” button.
spring-security-success-handler

If you like to use the <a title="Spring Boot" href="https://javadevjournal.com/spring-boot/" target="_blank" rel="noopener noreferrer">Spring Boot</a> CLI to generate the project structure, run the following command from the terminal.

spring init --name spring-security-custom-userdetailservice --dependencies=web,thymeleaf,security spring-security-success-handler
Using service at https://start.spring.io
Project extracted to '/Users/pooja/spring-security-success-handler'

Here is our pom.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.2.4.RELEASE</version>
      <relativePath />
      <!-- lookup parent from repository -->
   </parent>
   <groupId>com.javadevjournal</groupId>
   <artifactId>spring-security-success-handler</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>spring-security-success-handler</name>
   <description>How to redirect user to different page on login success</description>
   <properties>
      <java.version>1.8</java.version>
   </properties>
   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
         <scope>runtime</scope>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
         <exclusions>
            <exclusion>
               <groupId>org.junit.vintage</groupId>
               <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
         </exclusions>
      </dependency>
      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-test</artifactId>
         <scope>test</scope>
      </dependency>
   </dependencies>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>
</project>

We are adding MySQL as the dependencies to store user login details in the database and will customize the user service to get user login details from the DB.

2. Database Configuration

We are using Spring JPA for our application, which will handle most of the complex Database work (e.g. defining the JDBC connection and queries, etc,). Spring JPA need database information to store and retrieve information. We can provide this information using application.properties file. Let’s provide the connection information:

spring.jpa.generate-ddl=true
spring.datasource.url=jdbc:mysql://localhost:3306/spring-security-user-detail-service?useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

When we will start our application, Spring JPA will automatically create the required table structure for us.

3. Customer Model

We need a customer entity to store the user details in the database. I am keeping the customer model simple for this article, but the real world customer entity can be complex.

@Entity
public class CustomerModel {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;

    public Customer() {}
    //getter & setters methods
}

4. Returning Customer Information

For the Spring security, we need to load customer information from the database using username (email in our case). To achieve this, we will use the Spring data repository feature by extending JpaRepository interface. Let’s create a customer repository: 

import com.javadevjournal.jpa.entities.CustomerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CustomerRepository extends JpaRepository < CustomerEntity, Long > {
    CustomerEntity findByEmail(String email);
}

I am creating a method findByEmail since email id is also the username in my case, but you can name the method as findByUsername.

5. The UserDetailsService

This is the main configuration for our application. To load the customer details, we need to implement UserDetailsService interface. It uses spring Security UserDetailsService interface in order to lookup the username, password and GrantedAuthoritiesfor any user.

import com.javadevjournal.jpa.entities.CustomerEntity;
import com.javadevjournal.jpa.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailService implements UserDetailsService {

    @Autowired
    private CustomerRepository customerRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        final CustomerEntity customer = customerRepository.findByEmail(username);
        if (customer == null) {
            throw new UsernameNotFoundException(username);
        }
        UserDetails user = User.withUsername(customer.getEmail()).password(customer.getPassword()).authorities("USER").build();
        return user;
    }
}

This is the minimal code to show you how to create a custom UserDetailsService in Spring security. Let’s discuss some important points.

  1. We provided implementation for the UserDetailsService and loadUserByUsername method.
  2. We are using the CustomerRepository to find the user by username.
  3. This service will return the user details in the UserDetails object (we can also create a wrapper for this).

Please keep in mind following important points while creating this service for your application.

  1. We are using a hard-coded authority for the user, but for production application, we should load the permissions / authorities from the database.

The core idea is to return the User instance with populated values. You are free to implements UserDetails interface as per your requirement.

6. Spring Security Configuration

The last step is to configure Spring security to use our custom Spring security UserDetailsService to load user information during authentication. This is how our Spring security config class look like:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;

    @Bean
    public DaoAuthenticationProvider authProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authProvider());
    }
}
  1. The @EnableWebSecurity enable Spring Security’s web security support and provide the Spring MVC integration.
  2. We are extending WebSecurityConfigurerAdapter class, which provides a convenient base class for creating a WebSecurityConfigurer instance.
  3. We are injecting custom UserDetailsService in the DaoAuthenticationProvider. Spring security use this provider to load the customer information.

7. Application Testing

Let’s star tour application to see the custom provider in action.Once the application is up and running, open the following URL http://localhost:8080/login in a browser window. A login box will appear like below:

custom success handler

If you provide a correct username and password, it will allow you to login to the system. For invalid username or password, system will throw error back on the login screen:

Wrong Credential

Summary

In this post, we learn about Spring security UserDetailsService. We saw how to inject custom UserDetailsService to load customer information during authentication.

4 thoughts on “Spring Security UserDetailsService”

  1. Hello I am writing a little project and i have problem with login, after register user is right add to database, but after i’m trying login with users email and password I can’t do it
    here is my code:

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private CustomUserDetailService customUserDetailService;
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        };
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/").permitAll()
                    .antMatchers("/register").permitAll()
                    .antMatchers("/loginform").permitAll()
                    .anyRequest()
                    .authenticated()
                    .and().csrf().disable()
                    .formLogin()
                    .loginPage("/loginform").permitAll()
                    .defaultSuccessUrl("/campRegistration.html")
                    .permitAll();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(customUserDetailService).passwordEncoder(passwordEncoder());
        }
    
    @Service
    public class CustomUserDetailService implements UserDetailsService {
        private UserRepository userRepository;
    
        @Autowired
        public void setUserRepository(UserRepository userRepository) {
            this.userRepository = userRepository;
        }
    
        @Override
        public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
            User user=userRepository.findByEmail(email);
            if (user == null){
                throw new UsernameNotFoundException("User not found "+email);
            }
    //        UserDetails userD= org.springframework.security.core.userdetails.User.withUsername(user.getEmail()).password(user.getPassword()).authorities("USER").build();
            org.springframework.security.core.userdetails.User userDetails=
                    new org.springframework.security.core.userdetails.User(
                            user.getEmail(),
                            user.getPassword(),true,true,true,true,
                            convertAuthorities(user.getUserRoles())
                    );
            return userDetails;
        }
    
        private Set<GrantedAuthority> convertAuthorities(Set<UserRole> userRoles) {
            Set<GrantedAuthority> authorities=new HashSet<>();
            for (UserRole userRole : userRoles) {
                authorities.add(new SimpleGrantedAuthority(userRole.getRole()));
            }
            return authorities;
        }
    }
    

    loginform.html

    <form action="loginform" th:action="@{/loginform}"  method="post">
        <input type="text" id="username" name="username" placeholder="Login"><br/>
        <input type="password" id="password" name="passsword"  placeholder="Hasło"><br/>
        <input type="submit" class="btn btn-lg btn-primary" value="Zaloguj się">
    </form>
    <a class="btn btn-lg btn-primary" role="button" th:href="@{/register}">Zarejestruj się</a>
    

Comments are closed.