Skip to content

Custom UserDetailsService Of Spring Security#

Create Your Own Custom Implementation of UserDetailsService#

 #zoom  #zoom  #zoom

  • In this section we will create our custom implementation of UserDetailsService. So, why do we need to create a custom implementation of UserDetailsService of spring security?
  • The answer is that when you already have an database with users and authorities and you just want to migrate your existed users in the database into the spring security for using but your existing table is so different with the default implementation of spring security such as JdbcUserDetailsManager (See Configure Users With JdbcUserDetailsManager) . So we need to create your own custom UserDetailsService implementation

Dependencies#

  • We will use Spring Data JPA to integrate with the customers table that we defined in the database. So you will need to add some dependencies as below.
pom.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...

<dependency>  
    <groupId>org.projectlombok</groupId>  
    <artifactId>lombok</artifactId>  
    <version>1.18.22</version>  
    <scope>provided</scope>  
</dependency>  

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-web</artifactId>  
    <version>2.6.4</version>  
</dependency>  

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-security</artifactId>  
    <version>2.6.4</version>  
</dependency>  

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-jpa</artifactId>  
    <version>2.6.6</version>  
</dependency>  

<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>8.0.28</version>  
</dependency>

...

Prepare Tables In Database#

  • Now let's use the script below to create your own table in the database for storing customer users. You should note that the column's names and table's name don't need to have the same name as in the default implementation in spring security, you can put any name you want for table and column names here.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
CREATE TABlE customers (
id int NOT NULL AUTO_INCREMENT,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(255) NOT NULL,
PRIMARY KEY (id)
);

INSERT INTO worldbank.customers
(email, password, `role`)
VALUES('duc.nguyen@example.com', '12345', 'admin');

Create Entity Class#

  • Now let's create the CustomerEntity class which will be mapped with table customers in your database as below
CustomerEntity.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.spring.security.spring.security.custom.userDetailsService.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Table(name = "customers")
@Getter
@Setter
public class CustomerEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String email;
    private String password;
    private String role;

}

Create CustomerRepository#

  • Next, we will create the CustomerRepository interface for our CustomerEntity above with a method for getting the CustomerEntities by email.
CustomerRepository.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.spring.security.spring.security.custom.userDetailsService.repository;

import com.spring.security.spring.security.custom.userDetailsService.entity.CustomerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CustomerRepository extends JpaRepository<CustomerEntity, Integer> {

    List<CustomerEntity> findByEmail(String email);

}

Create Your Custom Implementation of UserDetails#

  • Now, we will create a class name SecurityCustomer for our custom UserDetails implementation. This class will have 1 attribute which is the CustomerEntity which we will load from our database. Because in our database we don't have column for AccountNonExpired, AccountNonLocked, CredentialsNonExpired and Enabled so we will set them as true.
SecurityCustomer.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.spring.security.spring.security.custom.userDetailsService.model;

import com.spring.security.spring.security.custom.userDetailsService.entity.CustomerEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class SecurityCustomer implements UserDetails {

    private final CustomerEntity customerEntity;

    public SecurityCustomer(CustomerEntity customerEntity) {
        this.customerEntity = customerEntity;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(this.customerEntity.getRole()));
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.customerEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return this.customerEntity.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

Create Your Custom Implementation Of UserDetailsService#

  • We will crate a class name BankUserDetailsService which will implement the UserDetailsService for our custom UserDetailsService implementation. So we will @Override the method loadUserByUsername of UserDetailsService
BankUserDetailsService.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.spring.security.spring.security.custom.userDetailsService.service;

import com.spring.security.spring.security.custom.userDetailsService.entity.CustomerEntity;
import com.spring.security.spring.security.custom.userDetailsService.model.SecurityCustomer;
import com.spring.security.spring.security.custom.userDetailsService.repository.CustomerRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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;

import java.util.List;

@Service
public class BankUserDetailsService implements UserDetailsService {

    private static final Logger LOGGER = LoggerFactory.getLogger(BankUserDetailsService.class);

    @Autowired
    private CustomerRepository customerRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LOGGER.info("BankUserDetailsService is triggered!");
        List<CustomerEntity> customerEntities = this.customerRepository.findByEmail(username);

        if (customerEntities.isEmpty()) {
            throw new UsernameNotFoundException("There are no user with email: " + username);
        }
        return new SecurityCustomer(customerEntities.get(0));
    }
}
  • We will use CustomerRepository to load our CustomerEntity from database then we will use this CustomerEntity to create the SecurityCustomer which is the implementation of UserDetails.
  • So we have done custom the UserDetailsService.

Configure Spring Security#

  • Now, let's open the class ProjectSecurityConfig and comment the configuration related to JdbcUserDetailsManager as below
ProjectSecurityConfig.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.spring.security.spring.security.custom.userDetailsService.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
public class ProjectSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     *
     * contact: Not Secure
     * notice: Not Secure
     * balance: Secure
     * Card: Secure
     * Loan: Secure
     * Account: Secure
     *
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/v1/accounts/**").authenticated()
                .antMatchers("/v1/balance").authenticated()
                .antMatchers("/v1/loan").authenticated()
                .antMatchers("/v1/card").authenticated()
                .antMatchers("/v1/contact").permitAll()
                .antMatchers("/v1/notice").permitAll()
                .and().formLogin()
                .and().httpBasic();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

}

Enable JPA Repositories#

  • Go to the main class and add the annotation @EnableJpaRepositories as below
CustomDefaultSpringSecurityApplication.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.spring.security.spring.security.custom.userDetailsService.repository;

import com.spring.security.spring.security.custom.userDetailsService.entity.CustomerEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface CustomerRepository extends JpaRepository<CustomerEntity, Integer> {

    List<CustomerEntity> findByEmail(String email);

}

Configure DataSource#

  • Now, let add the datasource configuration in the application.yml. Then start the application up to test.
    application.yml
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    #spring:
    #  security:
    #    user:
    #      name: user
    #      password: 12345
    #
    #server:
    #  servlet:
    #    session:
    #      timeout: 1m
    
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/worldbank?useUnicode=true&characterEncoding=UTF-8
        username: root
        password: password
    

Testing#

  • Now let's start your spring boot application and try to use the email/password that existed in the database for authentication and you will see the successful result as below.

 #zoom

See Also#

References#