Skip to content
Spring Boot sb security 3 min read

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 in getAuthorities()hasRole("ADMIN") won’t match.
  • @ElementCollection with LAZY fetch can trigger lazy-loading errors when authorities are read outside a transaction; EAGER (or a join fetch) avoids it.
Last updated June 13, 2026
Was this helpful?