Hello! Welcome back to our module on "Spring Security & REST API Design."
In our last lesson, we demystified Spring Security's architecture, exploring how the FilterChainProxy intercepts requests and how the SecurityContextHolder tracks the authenticated user. We saw that authentication is handled by components like the AuthenticationManager working within this filter chain.
Today, we will connect that architecture to a real-world data source. Your learning outcome is to implement authentication against a database by creating a custom UserDetailsService. This is a fundamental skill for any backend developer and a common topic in senior-level Java interviews. We'll replace Spring's default in-memory user with a system that can look up users from your application's database.
1. The Authentication Puzzle: Connecting to Your Data
In the previous lesson's architecture, we saw that the AuthenticationManager delegates to an AuthenticationProvider. For username/password authentication, this is typically the DaoAuthenticationProvider. The "DAO" (Data Access Object) part is the key: this provider needs to access your user data.
However, DaoAuthenticationProvider has no idea if your users are in a MySQL database, a PostgreSQL database, or even a simple file. To bridge this gap, Spring Security defines two crucial interfaces:
UserDetailsService: A service interface with a single method,loadUserByUsername(String username). Your job is to implement this method to fetch user data from your specific storage. It acts as a DAO for Spring Security.UserDetails: An interface that represents the user's core security information. YourUserDetailsServicemust return an object that implements this interface. It serves as an adapter between your application's user model (e.g., a JPA entity) and the data Spring Security requires, such as the username, the hashed password, and a list of authorities (roles/permissions).
The overall flow looks like this:
2. The Core Components in Detail
To fully grasp the concept before we dive into code, let's get a clear definition of the components involved.
Spring Security: Authentication and Authorization In-Depth
This article provides an excellent, concise explanation of the scenario where you have access to user passwords (e.g., in a database) and need to integrate with Spring Security.
Please read the sections titled '1. UserDetailsService: Having access to the user’s password', 'PasswordEncoders', and the summary 'Summary: Having access to the user’s password'. Focus on understanding the distinct roles of UserDetailsService and PasswordEncoder and why both are necessary.
The key takeaways from this reading are:
- If you manage your own users, you must provide a
UserDetailsServicebean. - This service's job is to load user data by username only. Spring Security handles the password comparison.
- You must also provide a
PasswordEncoderbean so Spring Security knows which hashing algorithm to use. Storing plain-text passwords is a critical security vulnerability.
3. Step-by-Step Implementation
Now, let's build a complete database authentication solution. We'll follow a common and clean approach using the Decorator pattern, where we create a dedicated UserDetails implementation that wraps our JPA User entity.
This video provides a practical, step-by-step guide that we will follow. I'll break down the key segments and add context.
[Spring Boot Security] #19 Database Authentication - Implement User Details Service
This video by Dan Geabunea demonstrates how to implement a custom UserDetailsService from scratch. We will use it to guide our implementation.
Watch the video from the beginning to 12:40. It covers the entire process of creating the UserDetails and UserDetailsService implementations. I'll highlight the most important steps below.
Step 3.1: Create the UserDetails Adapter (The Decorator)
(Video segment: 01:54 - 09:12)
Instead of making your JPA @Entity class implement UserDetails directly, it's often cleaner to create a separate class. This separates your domain model from security-specific concerns.
- Create a
UserPrincipalclass that implementsUserDetails. - Add a private final
Userfield (your JPA entity) to this class and initialize it in the constructor. This is the "decorating" part. - Implement the
UserDetailsmethods (getUsername,getPassword, etc.) by delegating the calls to the wrappedUserentity (e.g.,return this.user.getPassword();). - Implement
getAuthorities(). This is crucial. You need to convert the roles and/or permissions from yourUserentity into aCollection<GrantedAuthority>. A common way is to createSimpleGrantedAuthorityobjects. Remember the convention: roles are typically prefixed withROLE_(e.g.,ROLE_ADMIN). The video demonstrates combining two separate lists (roles and permissions) into one list of authorities.
// Example UserPrincipal
public class UserPrincipal implements UserDetails {
private final User user; // Your JPA User entity
public UserPrincipal(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// Example: converting a Set<Role> from your User entity
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword(); // The hashed password from the DB
}
@Override
public String getUsername() {
return user.getUsername();
}
// For simplicity, other methods can return true
@Override
public boolean isAccountNonExpired() { return true; }
// ... etc.
}
Step 3.2: Implement the UserDetailsService
(Video segment: 09:12 - 12:40)
This is the service that Spring Security will call.
- Create a
@Serviceclass (e.g.,CustomUserDetailsService) that implementsUserDetailsService. - Inject your
UserRepository(e.g.,JpaRepository<User, Long>). - Implement the
loadUserByUsername(String username)method:- Use the injected repository to find the user in the database (e.g.,
userRepository.findByUsername(username)). - If the user is not found, you must throw a
UsernameNotFoundException. Returningnullviolates the interface contract and can cause unexpected errors. - If the user is found, wrap the
Userentity in yourUserPrincipaladapter and return it:return new UserPrincipal(user);.
- Use the injected repository to find the user in the database (e.g.,
Test your understanding!
An interviewer asks: "In your UserDetailsService implementation, what should happen if a user with the given username doesn't exist in the database?"
Show answer
Your implementation should throw a UsernameNotFoundException. This is a specific AuthenticationException subclass that Spring Security is designed to handle. It signals a failed authentication attempt due to an invalid username. Returning null or a generic exception is incorrect and breaks the UserDetailsService contract.
Step 3.3: Configure the PasswordEncoder and AuthenticationProvider
Now we need to tell Spring Security to use our new components. The modern, component-based approach in Spring Security 6+ makes this very clean.
Implementing Database Authentication and Authorization ...
This article from GeeksForGeeks shows how to configure the security components using the latest Spring Security 6 style, which avoids the deprecated WebSecurityConfigurerAdapter.
Review the code in the section titled 'Final and Important Step: Configure Spring Security'. Focus on how the AuthenticationProvider bean is created and configured with the UserDetailsService and PasswordEncoder.
In your @Configuration class for security, you'll define two essential beans:
-
PasswordEncoderBean: This tells Spring Security how to hash and verify passwords.BCryptPasswordEncoderis the strong, modern standard.@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -
AuthenticationProviderBean: This bean explicitly wires together your customUserDetailsServiceand thePasswordEncoder.@Bean public AuthenticationProvider authenticationProvider(UserService userService) { // Assuming your service is named UserService DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userService); // Here's the connection! provider.setPasswordEncoder(passwordEncoder()); // And here! return provider; }
By defining these beans, Spring Boot's auto-configuration will pick them up and use them in the authentication process. You have now successfully plugged your database into the security architecture.
Conclusion
In this lesson, you've bridged the gap between Spring Security's abstract architecture and a concrete database implementation. This is a critical pattern for building secure, real-world applications and a must-know for technical interviews.
Key Takeaways:
UserDetailsServiceis the contract you implement to tell Spring Security how to load a user from your data store.UserDetailsis the contract that adapts your application's user model into a format Spring Security can understand.- The Decorator Pattern (using a wrapper class like
UserPrincipal) is a clean way to implementUserDetailswithout cluttering your domain entity. - You must always provide a
PasswordEncoderbean (likeBCryptPasswordEncoder) to securely handle passwords. - These components are wired together in your security configuration by defining an
AuthenticationProviderbean that uses your customUserDetailsServiceandPasswordEncoder.
Preview of the Next Lesson:
Our current setup relies on form-login and sessions, which is great for traditional web applications. However, modern REST APIs, especially in a microservices context, often need to be stateless. In our next lesson, we'll address this by learning how to explain the structure of a JSON Web Token (JWT) and its role in stateless authentication. We'll prepare to replace session-based security with token-based security.