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)
- Here the
UserRole
is to show how a single user can have multiple roles (@OneToMany). - 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.
- 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)
UserDetails
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.