🏠SecurityUserDetailsService : Loading UserDetails from database

UserDetailsService : Loading UserDetails from database

In the last post, We have seen how easy it is to set up an in-memory UserDetailsService and dynamically add users to the applications. However, we all know that the implementation is only good for demos and short-lived applications. Once these applications are stopped, All the information about the users is lost. This is why most enterprise applications keep their users’ account information in a database. With that being said, We will see how we can load user details from a database table.

Database entities

In order to load user information from the database, we need to use spring JDBC or spring JPA. For the sake of completeness, I’m using spring JPA and here is a simple UserAccount and UserRole entities.

@Entity @Data public class UserAccount { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(unique = true) private String username; private String password; private boolean active; @OneToMany private List<UserRole> userRoles; }
Code language: PHP (php)
@Entity @Data public class UserRole { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @ManyToOne private UserAccount userAccount; private String role; }
Code language: CSS (css)
  1. Here the UserRole is to show how a single user can have multiple roles (@OneToMany).
  2. The username column is marked as unique due to the nature of how usernames should be. However, it’s up to you how you want to design the database entries.
  3. I’m using Lombok hence there are no getters and setters.

With the entities ready, let’s write the necessary repository methods for our CustomUserDetailService. The following definition would return an UserAccount entity based on the username.

@Repository public interface UserAccountRepository extends JpaRepository<UserAccount, Integer> { UserAccount findByUsername(String username); }
Code language: PHP (php)


The UserDetailsService service interface is supposed to return an implementation of org.springframework.security.core.userdetails.UserDetails. So first we need to define a CustomUserDetails class backed by an UserAccount. Here is how I implemented them. However, it is up to you to implement this class differently if you have to.

public class CustomUserDetails implements UserDetails { private final UserAccount userAccount; public CustomUserDetails(UserAccount userAccount) { this.userAccount = userAccount; } @Override public String getUsername() { return userAccount.getUsername(); } @Override public String getPassword() { return userAccount.getPassword(); } @Override public boolean isAccountNonExpired() { return userAccount.isActive(); } @Override public boolean isAccountNonLocked() { return userAccount.isActive(); } @Override public boolean isCredentialsNonExpired() { return userAccount.isActive(); } @Override public boolean isEnabled() { return userAccount.isActive(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return Collections.singletonList(new GrantedAuthority() { @Override public String getAuthority() { return "USER"; } }); } }
Code language: PHP (php)

The getAuthorities() method of UserDetails needs a list of GrantedAuthority. For now, I have hardcoded it to return only USER the role. Also, I have written a getUserAccount() so that I can use this to get hold of the current user entity.

Loading user details from the database

With all the above set, All we need is UserDetailService implementation. As we have already established our database entities and repositories, let’s write our performance and mark it a bean with the help of @Component annotation.

@Component public class DatabaseUserDetailsService implements UserDetailsService { private final UserAccountRepository userAccountRepository; public DatabaseUserDetailsService(UserAccountRepository userAccountRepository) { this.userAccountRepository = userAccountRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserAccount userAccount = userAccountRepository.findByUsername(username); if (userAccount == null) { throw new UsernameNotFoundException("User with username [" + username + "] not found in the system"); } return new CustomUserDetails(userAccount); } }
Code language: PHP (php)

This is as simple as it can get. The contract for this method is that if the system is not able to find a user for a given username, the method should throw a UsernameNotFoundException message. Once the method gets a UserAccount record, It is converted into CustomUserDetails and presented in a security context.

Register and login

To simulate user creation in the DB, Let’s create a simple registration form and a /register endpoint.

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Register a new user</title> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.min.css"/> </head> <body class="container mt-5"> <h1 class="text-center">Register</h1> <form method="post" action="/register" autocomplete="off"> <div class="row"> <div class="col form-group"> <label for="firstName">First Name</label> <input type="text" class="form-control" id="firstName" name="firstName" autocomplete="new-password" required placeholder="First Name"> </div> <div class="col form-group"> <label for="lastName">Last Name</label> <input type="text" class="form-control" id="lastName" name="lastName" autocomplete="new-password" required placeholder="Last Name"> </div> </div> <div class="form-group"> <label for="username">Username</label> <input type="text" class="form-control" id="username" name="username" autocomplete="new-password" required placeholder="Enter username"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required autocomplete="new-password"> </div> <input type="submit" class="btn btn-primary btn-block btn-lg" value="Register"/> </form> </body> </html>
Code language: HTML, XML (xml)
@RestController public class UserController { private final UserAccountRepository userAccountRepository; private final PasswordEncoder passwordEncoder; public UserController(UserAccountRepository userAccountRepository, PasswordEncoder passwordEncoder) { this.userAccountRepository = userAccountRepository; this.passwordEncoder = passwordEncoder; } @PostMapping("/register") public UserAccount register(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName) { UserAccount userAccount = new UserAccount(); userAccount.setFirstName(firstName); userAccount.setLastName(lastName); userAccount.setUsername(username); userAccount.setPassword(passwordEncoder.encode(password)); userAccount.setActive(true); return userAccountRepository.save(userAccount); } }
Code language: PHP (php)

Storing passwords in DB as plain text is not advised for production deployments. This is why spring boot requires a password encoder to encode and store and validate the password in the database. The current industry standard is to use BCryptPasswordEncoder. You need to define it as a bean somewhere in your @Configuration class as shown below. You can find details about password encoders in an upcoming post.

@Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
Code language: JavaScript (javascript)

To make sure register.html and /register accessible without requiring login, exclude them from web security.

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) { web.ignoring() .antMatchers(HttpMethod.GET, "/register.html") .antMatchers(HttpMethod.POST, "/register"); } }
Code language: PHP (php)

The Result

With all the above in place, let’s test by registering a user and logging in using the same application.

For the sake of the demo, I used the h2 embedded database. However, with JPA it doesn’t matter which database you want to use. As long as appropriate jdbc drivers are available and table structure is matching your JPA entities, you are good.

You can try out this project yourself by checking out this GitHub repository.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *