Database Authentication
Real applications store users in a database, not in memory. Spring Security loads them through a UserDetailsService — a single-method interface that maps a username to a UserDetails. By backing that service with a Spring Data JPA repository, you get database-driven login with very little code. This page covers the entity, the repository, the service, the provider wiring, and the registration flow.
The pieces
Login attempt
│
▼
DaoAuthenticationProvider
│ loadUserByUsername(name)
▼
CustomUserDetailsService ── UserRepository ──► users table
│ returns UserDetails
▼
PasswordEncoder.matches(rawPassword, storedHash)
▼
Authentication (success / BadCredentialsException)
The User entity
Store users in a JPA entity. The cleanest approach is to keep your domain entity and adapt it to UserDetails — but implementing UserDetails directly is also common and shown here for clarity.
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.*;
@Entity
@Table(name = "users")
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class UserAccount implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password; // BCrypt hash, never plaintext
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
private Set<Role> roles = new HashSet<>();
private boolean enabled = true;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(r -> new SimpleGrantedAuthority("ROLE_" + r.name()))
.toList();
}
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return enabled; }
}
enum Role { USER, ADMIN }
Tip: If you prefer to keep persistence and security concerns separate, leave your entity as a plain POJO and wrap it in an adapter class that implements
UserDetails. It avoids leaking security interfaces into your domain model.
The repository
A standard JPA repository with a finder by username:
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<UserAccount, Long> {
Optional<UserAccount> findByUsername(String username);
boolean existsByUsername(String username);
}
The custom UserDetailsService
Implement UserDetailsService and throw UsernameNotFoundException when the user is missing. Because the entity already implements UserDetails, you can return it directly.
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException("User not found: " + username));
}
}
Wiring the DaoAuthenticationProvider
With a UserDetailsService bean and a PasswordEncoder bean on the classpath, Spring Boot auto-configures a DaoAuthenticationProvider for you. You can also declare it explicitly when you want control or have multiple providers:
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
The exposed AuthenticationManager bean is what a login endpoint uses to verify credentials programmatically (see JWT login flows).
Registration flow
Registration creates a new user with an encoded password. Never store the raw value.
@Service
@RequiredArgsConstructor
public class RegistrationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserAccount register(String username, String rawPassword) {
if (userRepository.existsByUsername(username)) {
throw new IllegalArgumentException("Username already taken");
}
UserAccount account = UserAccount.builder()
.username(username)
.password(passwordEncoder.encode(rawPassword)) // hash it
.roles(Set.of(Role.USER))
.enabled(true)
.build();
return userRepository.save(account);
}
}
Login request after registering:
curl -u newuser:secret123 http://localhost:8080/api/profile
Output:
HTTP/1.1 200 OK
Pitfalls
- Storing the raw password instead of
passwordEncoder.encode(...)— login will always fail. - Forgetting the
ROLE_prefix ingetAuthorities()—hasRole("ADMIN")won’t match. @ElementCollectionwithLAZYfetch can trigger lazy-loading errors when authorities are read outside a transaction;EAGER(or a join fetch) avoids it.