Careeroza
UserDetailsService with a Database
The User Entity
ShopEase stores customers in a users table (JPA mapping details are in the Hibernate & JPA material):
package com.shopease.user;
import jakarta.persistence.*;
@Entity
@Table(name = "users") // "user" is a reserved word in PostgreSQL
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String passwordHash; // BCrypt hash, never the raw password
@Column(nullable = false)
private String role; // "CUSTOMER" or "ADMIN"
private boolean enabled = true;
protected User() {}
public User(String email, String passwordHash, String role) {
this.email = email;
this.passwordHash = passwordHash;
this.role = role;
}
public Long getId() { return id; }
public String getEmail() { return email; }
public String getPasswordHash() { return passwordHash; }
public String getRole() { return role; }
public boolean isEnabled() { return enabled; }
}public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
}Implementing UserDetailsService
package com.shopease.security;
import com.shopease.user.UserRepository;
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 ShopEaseUserDetailsService implements UserDetailsService {
private final UserRepository users;
public ShopEaseUserDetailsService(UserRepository users) {
this.users = users;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
com.shopease.user.User user = users.findByEmail(email.toLowerCase())
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
return org.springframework.security.core.userdetails.User
.withUsername(user.getEmail())
.password(user.getPasswordHash())
.roles(user.getRole())
.disabled(!user.isEnabled())
.build();
}
}- Annotating it with
@Serviceis enough: with oneUserDetailsServicebean and onePasswordEncoderbean, Spring wires theDaoAuthenticationProviderfor you. - Our entity is also called
User, so the Spring builder is written with its full name. - The "username" can be anything unique: here it is the email address.
Note: Spring returns the same "Bad credentials" error for an unknown email and a wrong password, so attackers cannot find out which emails are registered.
Simulating the Lookup
Example
What DaoAuthenticationProvider does with your service, in plain Java (with a PBKDF2 encoder standing in for BCrypt):
import java.security.MessageDigest;
import java.util.*;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
public class Main {
record UserDetails(String username, byte[] hash, byte[] salt, List<String> authorities, boolean enabled) {}
static byte[] hash(String raw, byte[] salt) throws Exception {
return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
.generateSecret(new PBEKeySpec(raw.toCharArray(), salt, 10_000, 256)).getEncoded();
}
static final Map<String, UserDetails> DB = new HashMap<>();
static String login(String email, String password) throws Exception {
UserDetails u = DB.get(email.toLowerCase()); // loadUserByUsername
if (u == null) return "401 Bad credentials"; // same message as wrong password
if (!u.enabled()) return "401 User is disabled";
boolean ok = MessageDigest.isEqual(u.hash(), hash(password, u.salt())); // passwordEncoder.matches
return ok ? "200 authenticated " + u.username() + " " + u.authorities() : "401 Bad credentials";
}
public static void main(String[] args) throws Exception {
byte[] s1 = {1, 2, 3, 4, 5, 6, 7, 8}, s2 = {8, 7, 6, 5, 4, 3, 2, 1};
DB.put("[email protected]", new UserDetails("[email protected]", hash("asha123", s1), s1, List.of("ROLE_CUSTOMER"), true));
DB.put("[email protected]", new UserDetails("[email protected]", hash("old123", s2), s2, List.of("ROLE_CUSTOMER"), false));
System.out.println(login("[email protected]", "asha123"));
System.out.println(login("[email protected]", "asha124"));
System.out.println(login("[email protected]", "x"));
System.out.println(login("[email protected]", "old123"));
}
}Output
200 authenticated [email protected] [ROLE_CUSTOMER] 401 Bad credentials 401 Bad credentials 401 User is disabled