We are still cooking the magic in the way!
Users & UserDetailsService
Users & UserDetailsService
Spring Security does not talk to your database directly. Instead it delegates the job of loading a user to a single interface: UserDetailsService. Whatever backing store you use — an in-memory map, a relational database, LDAP, or a remote microservice — you wrap it in a class that implements this interface and Spring Security works the same way. This decoupling is one of the clearest design decisions in the framework, and understanding it is the key to customizing authentication.
The Core Contracts
Two interfaces carry almost all of the weight:
UserDetailsService— a single-method interface. You implementloadUserByUsername(String username)and return aUserDetailsobject. Spring Security calls it during authentication.UserDetails— represents the principal. It exposes the hashed password, the collection ofGrantedAuthorityobjects (roles/permissions), and four boolean flags:isEnabled,isAccountNonExpired,isAccountNonLocked,isCredentialsNonExpired.
If the username does not exist, loadUserByUsername must throw UsernameNotFoundException, never return null. Returning null causes a NullPointerException buried inside the framework — always throw.
In-Memory Users — Fast Setup for Development
The quickest way to configure users is to provide an InMemoryUserDetailsManager bean. This is appropriate for demos, automated tests, and local development — not for production.
{bcrypt}$2a$...) or is supplied through a PasswordEncoder bean. If you use User.withDefaultPasswordEncoder() you will see a deprecation warning — it is only for samples, not real code. Always wire in a PasswordEncoder bean explicitly.
The roles("USER") helper is syntactic sugar: it creates a GrantedAuthority with the value ROLE_USER. If you need authority names that do not follow the ROLE_ prefix convention, use .authorities("READ_REPORTS", "WRITE_REPORTS") instead.
Loading Users from a Database — The Real Pattern
In a production application your users live in a database table managed by JPA. You implement UserDetailsService and inject your Spring Data repository:
Spring Security auto-detects a single UserDetailsService bean and wires it into the authentication provider. You do not need any extra configuration to connect the two.
loadUserByUsername is called inside Spring Security's authentication machinery, which is outside any open Hibernate session. If you load roles lazily you will hit a LazyInitializationException at runtime. Mark the roles collection FetchType.EAGER, or call Hibernate.initialize(user.getRoles()) while the transaction is still open, or use a JPQL JOIN FETCH query.
Combining UserDetailsService with SecurityFilterChain
You wire a custom UserDetailsService into your security configuration by injecting it and building an AuthenticationProvider:
DaoAuthenticationProvider is the standard implementation that calls loadUserByUsername, then verifies the submitted password against the stored hash using your PasswordEncoder. It also checks the four boolean flags on UserDetails before granting access — this is where you implement account locking, expiry, and disabling without writing any authentication logic yourself.
The UserDetails Flags and Why They Matter
isEnabled()— returnsfalseto prevent login for soft-deleted or unverified accounts.isAccountNonLocked()— returnsfalseafter too many failed login attempts.isAccountNonExpired()— returnsfalsefor accounts whose subscription has lapsed.isCredentialsNonExpired()— returnsfalseto force a password reset after a set interval.
Each flag throws a different AuthenticationException subclass, so your error-handling code can distinguish between a locked account and an expired password and show the user an appropriate message.
UsernameNotFoundException with a generic message ("Bad credentials", not "User alice does not exist"). Spring Security's DaoAuthenticationProvider masks UsernameNotFoundException by default (it re-throws it as a generic BadCredentialsException) to prevent user enumeration attacks. Do not disable this behaviour.
Distributed-Systems Consideration: Stateless Services
In a microservices architecture, calling loadUserByUsername — and hitting the database — on every request is expensive and creates a coupling between your service and the user store. The typical solution is to authenticate once (via a gateway or auth service), issue a signed token (JWT), and have downstream services validate the token locally without calling UserDetailsService at all. You will explore this pattern in Lesson 8 (Authorization Rules) and in the dedicated JWT lesson. For now, understand that UserDetailsService is the authentication-time hook; token validation replaces it in stateless flows.
Summary
UserDetailsService is the single extension point Spring Security gives you for loading user data. Implement it to integrate any backing store: InMemoryUserDetailsManager for tests, a JPA-backed service for production. Return a correctly populated UserDetails object — with hashed password, authorities, and accurate flag values — and wire it into a DaoAuthenticationProvider. From that point the framework handles password verification, flag checking, and authentication exception routing for you.