In the previous article, we learned how Spring Security securely stores passwords using BCryptPasswordEncoder.
While in-memory users are useful for learning and testing, real-world applications store users in a database. When a user attempts to log in, Spring Security fetches user details from the database, validates the password, and grants access based on roles and permissions.
In this article, we will implement database authentication using:
Spring Security 7
Spring Boot 4
Java 21
Spring Data JPA
PostgreSQL (or MySQL)
BCryptPasswordEncoder
This approach is used in production systems such as e-commerce applications, HRMS platforms, banking systems, SaaS products, and enterprise applications.
Why In-Memory Authentication is Not Enough
Previously we created users like this:
UserDetailsuser=User.builder()
.username("john")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
Problems:
Users disappear after application restart.
New users cannot register.
Password updates are impossible.
Role management becomes difficult.
Not suitable for production.
Instead, users should be stored in a database.
Authentication Flow with Database
Client Login Request
↓
Spring Security
↓
UserDetailsService
↓
UserRepository
↓
Database
↓
User Found
↓
Password Validation
↓
Authentication Success
This is the standard authentication flow used by most enterprise applications.
Creating User Entity
First, create a database entity.
@Entity
@Table(name="users")
publicclassUser {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
privateLongid;
@Column(nullable=false,
unique=true)
privateStringusername;
@Column(nullable=false)
privateStringpassword;
@Column(nullable=false)
privateStringrole;
// Getters and Setters
}
Understanding the Entity
id
Unique identifier for each user.
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
privateLongid;
username
Stores the login username.
@Column(nullable=false,
unique=true)
privateStringusername;
The unique constraint prevents duplicate usernames.
password
Stores the encoded password.
privateStringpassword;
Never store plain-text passwords.
role
Stores the user role.
Examples:
ROLE_USER
ROLE_ADMIN
ROLE_MANAGER
Creating Repository
@Repository
publicinterfaceUserRepository
extendsJpaRepository<User,Long> {
Optional<User>findByUsername(Stringusername);
}
Why Repository is Required
Spring Security needs a way to fetch users from the database.
The method:
findByUsername(String username)
is used during authentication.
Creating Custom UserDetailsService
Spring Security uses UserDetailsService to load user information.
@Service
@RequiredArgsConstructor
publicclassCustomUserDetailsService
implementsUserDetailsService {
privatefinalUserRepositoryuserRepository;
@Override
publicUserDetailsloadUserByUsername(
Stringusername)
throwsUsernameNotFoundException {
Useruser=userRepository
.findByUsername(username)
.orElseThrow(() ->
newUsernameNotFoundException(
"User not found"));
returnorg.springframework.security.core.userdetails
.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
Understanding Every Line
loadUserByUsername()
Spring Security automatically calls this method during login.
loadUserByUsername(String username)
Fetching User
userRepository.findByUsername(username)
Queries the database.
UsernameNotFoundException
thrownewUsernameNotFoundException(
"User not found");
Thrown when no matching user exists.
Returning UserDetails
returnUser.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build();
Spring Security converts database data into a format it understands.
PasswordEncoder Configuration
@Configuration
publicclassPasswordConfig {
@Bean
publicPasswordEncoderpasswordEncoder() {
returnnewBCryptPasswordEncoder();
}
}
This encoder will be used for:
Registration
Login
Password updates
Security Configuration
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
publicclassSecurityConfig {
privatefinalCustomUserDetailsService
userDetailsService;
@Bean
publicSecurityFilterChainsecurityFilterChain(
HttpSecurityhttp)
throwsException {
returnhttp
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth ->auth
.requestMatchers(
"/api/public")
.permitAll()
.requestMatchers(
"/api/admin")
.hasRole("ADMIN")
.anyRequest()
.authenticated())
.userDetailsService(
userDetailsService)
.httpBasic(
Customizer.withDefaults())
.build();
}
}
Creating User Registration Logic
When users register, passwords must be encoded.
@Service
@RequiredArgsConstructor
publicclassUserService {
privatefinalUserRepositoryrepository;
privatefinalPasswordEncoderpasswordEncoder;
publicUserregister(UserRequestrequest) {
Useruser=newUser();
user.setUsername(
request.getUsername());
user.setPassword(
passwordEncoder.encode(
request.getPassword()));
user.setRole("USER");
returnrepository.save(user);
}
}
Example Registration Request
{
"username":"john",
"password":"password123"
}
Stored Database Record
Instead of:
password123
Database stores:
$2a$10$J8gfKlmN7...
This protects user credentials.
Real-World Example
Imagine an HR Management System.
Users table:
Username | Role |
|---|---|
john | USER |
admin | ADMIN |
manager | MANAGER |
When login occurs:
Spring Security receives username.
Calls
CustomUserDetailsService.Fetches user from database.
Validates password using BCrypt.
Loads user roles.
Grants access.
This is how most enterprise applications authenticate users.
Common Mistakes
Returning Entity Directly
Wrong:
returnuser;
Spring Security expects UserDetails.
Always convert entity to UserDetails.
Saving Plain Passwords
Wrong:
user.setPassword(password);
Correct:
user.setPassword(
passwordEncoder.encode(password));
Missing Username Index
Searching users without an index becomes slow.
Always make username unique.
@Column(unique=true)
Loading All Users
Wrong:
findAll()
Authentication should fetch only the required user.
Use:
findByUsername()
Production-Level Best Practices
Use Database Authentication
Avoid InMemoryUserDetailsManager in production.
Store Encoded Passwords
Always use BCrypt.
Separate Roles and Permissions
Instead of:
ADMIN
USER
Enterprise systems often use:
READ_USERS
WRITE_USERS
DELETE_USERS
for finer control.
Enable Audit Logging
Track:
Login attempts
Password changes
Role changes
Failed authentications
Use Account Locking
After multiple failed login attempts:
Account Locked
This helps prevent brute-force attacks.
Summary
In this article, we learned:
Why database authentication is required.
Creating User entities.
Creating UserRepository.
Implementing CustomUserDetailsService.
Loading users from a database.
Using BCryptPasswordEncoder.
User registration flow.
Production authentication best practices.
In Part 5, we will explore Role-Based and Authority-Based Access Control, where we will build a flexible permission system using roles, authorities, and fine-grained authorization rules used in large-scale enterprise applications.



