Appearance
Spring Boot Security Part 2 | Multiple User Creation inMemory and DB Storage
Introduction: The User Registry Analogy
Before a nightclub can check IDs at the door, it needs a guest list. The bouncer cannot authenticate anyone without knowing who is expected. Similarly, before Spring Security can authenticate users, it needs to know who the valid users are, what their passwords are, and what roles they hold.
This chapter covers the foundational first step: user creation and storage. You must set this up before any authentication method (form login, basic auth, JWT, OAuth2) can work. This is not optional — it is the bedrock.
Spring Security provides three approaches to user creation:
- In memory (for development and testing)
- Database storage via JDBC (for production)
- Custom
UserDetailsService(industry standard for full control)
Why User Creation Comes First
When a login request arrives, Spring Security's authentication flow calls UserDetailsService.loadUserByUsername(username). This method must return a UserDetails object containing the username, encoded password, and roles. If you have not set up any user store, Spring Security cannot authenticate anyone.
The three approaches differ in where the user data lives, but the interface Spring Security uses (UserDetailsService) remains the same.
Approach 1: InMemory User Details Manager
InMemoryUserDetailsManager stores users in application memory. This is useful for:
- Development and local testing
- Demos and prototypes
- Integration tests
Important: Users defined in memory are lost when the application restarts. This is never used in production.
Basic Setup
java
package com.example.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
// Create user with USER role
UserDetails user = User.builder()
.username("alice")
.password(encoder.encode("password123"))
.roles("USER") // Internally stored as ROLE_USER
.build();
// Create admin with ADMIN and USER roles
UserDetails admin = User.builder()
.username("bob")
.password(encoder.encode("admin456"))
.roles("ADMIN", "USER") // Multiple roles
.build();
// Create a read only user
UserDetails readOnly = User.builder()
.username("charlie")
.password(encoder.encode("readonly789"))
.roles("VIEWER")
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
return new InMemoryUserDetailsManager(user, admin, readOnly);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/view/**").hasAnyRole("VIEWER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form.permitAll())
.logout(logout -> logout.permitAll());
return http.build();
}
}Understanding roles() vs authorities()
java
// roles("USER") is a shortcut — it automatically prepends ROLE_
UserDetails user1 = User.builder()
.username("alice")
.password(encoded)
.roles("USER") // stored as ROLE_USER
.build();
// authorities() sets the value exactly as given — no prefix added
UserDetails user2 = User.builder()
.username("bob")
.password(encoded)
.authorities("ROLE_USER", "READ_PRIVILEGE") // exact values
.build();Approach 2: JDBC User Details Manager
JdbcUserDetailsManager stores users in a relational database using Spring's JDBC support. Spring Security expects specific table schemas for this.
Default Schema
Spring Security provides a default schema. You can let Spring create it automatically or run the DDL yourself.
sql
-- Spring Security's default schema (users table)
CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(500) NOT NULL,
enabled BOOLEAN NOT NULL
);
-- Spring Security's default schema (authorities table)
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users(username)
);
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);Maven Dependencies
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>JDBC Configuration
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private DataSource dataSource;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
// Create users only if they do not exist yet
if (!manager.userExists("alice")) {
UserDetails user = User.builder()
.username("alice")
.password(passwordEncoder().encode("password123"))
.roles("USER")
.build();
manager.createUser(user);
}
if (!manager.userExists("bob")) {
UserDetails admin = User.builder()
.username("bob")
.password(passwordEncoder().encode("admin456"))
.roles("ADMIN")
.build();
manager.createUser(admin);
}
return manager;
}
}yaml
# application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
sql:
init:
schema-locations: classpath:schema.sql # Spring Security's default schemaApproach 3: Custom UserDetailsService (Industry Standard)
In real applications, you have your own User entity with custom fields. The JdbcUserDetailsManager is too rigid — it forces you to use its table structure. The industry standard is to implement UserDetailsService yourself, giving you full control over the database schema and user model.
Step 1: User Entity
java
package com.example.security.entity;
import jakarta.persistence.*;
import java.util.Set;
@Entity
@Table(name = "app_users")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean enabled = true;
// Store roles as a comma separated string or use a separate table
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles;
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public Set<String> getRoles() { return roles; }
public void setRoles(Set<String> roles) { this.roles = roles; }
}Step 2: User Repository
java
package com.example.security.repository;
import com.example.security.entity.AppUser;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByUsername(String username);
}Step 3: Custom UserDetailsService Implementation
java
package com.example.security.service;
import com.example.security.entity.AppUser;
import com.example.security.repository.UserRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.stream.Collectors;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Fetch user from database
AppUser appUser = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found with username: " + username
));
// Convert roles to GrantedAuthority list
var authorities = appUser.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
// Return Spring Security's UserDetails object
return new User(
appUser.getUsername(),
appUser.getPassword(),
appUser.isEnabled(),
true, // accountNonExpired
true, // credentialsNonExpired
true, // accountNonLocked
authorities
);
}
}Step 4: User Registration Service
java
package com.example.security.service;
import com.example.security.entity.AppUser;
import com.example.security.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public AppUser registerUser(String username, String rawPassword, Set<String> roles) {
if (userRepository.findByUsername(username).isPresent()) {
throw new IllegalArgumentException("Username already exists: " + username);
}
AppUser user = new AppUser();
user.setUsername(username);
// ALWAYS encode before saving — never store plain text
user.setPassword(passwordEncoder.encode(rawPassword));
user.setRoles(roles);
user.setEnabled(true);
return userRepository.save(user);
}
}Step 5: Security Config with Custom UserDetailsService
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public SecurityConfig(CustomUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/register", "/api/login").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService); // wire your custom service
return http.build();
}
}Comparison: Which Approach to Use?
| Approach | When to Use | Pros | Cons |
|---|---|---|---|
| InMemoryUserDetailsManager | Dev, testing, demos | Simple, no DB needed | Not persistent, not scalable |
| JdbcUserDetailsManager | Simple apps with fixed schema | Built in, no custom code | Rigid schema, limited flexibility |
| Custom UserDetailsService | Production, real apps | Full control, any schema | More code to write |
In the real world, the custom UserDetailsService approach is the standard. Every serious application has its own user model with additional fields (email, profile picture, two factor auth settings, etc.) that do not fit the Spring Security default schema.
The PasswordEncoder Deep Dive
Why You Cannot Skip It
Spring Security will refuse to authenticate users if the stored password does not use a {id} prefix or a registered encoder. If you see the error There is no PasswordEncoder mapped for the id "null", it means the password is stored as plain text.
Encoding Strategies
java
// BCrypt - recommended for passwords (slow by design, salt included)
PasswordEncoder bcrypt = new BCryptPasswordEncoder();
PasswordEncoder bcrypt10 = new BCryptPasswordEncoder(10); // strength 10 (default)
PasswordEncoder bcrypt12 = new BCryptPasswordEncoder(12); // stronger, slower
// NoOp - NEVER use in production, only for debugging
PasswordEncoder noOp = NoOpPasswordEncoder.getInstance();
// DelegatingPasswordEncoder - supports multiple encoders, useful for migrations
PasswordEncoder delegating = PasswordEncoderFactories.createDelegatingPasswordEncoder();The DelegatingPasswordEncoder stores passwords with a prefix like {bcrypt}$2a$10$.... This allows you to migrate from one encoder to another without breaking existing users.
Data Initialization on Startup
In real applications, you may want to seed the database with an initial admin user.
java
package com.example.security.config;
import com.example.security.service.UserService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Set;
@Configuration
public class DataInitializer {
@Bean
public CommandLineRunner initUsers(UserService userService) {
return args -> {
try {
userService.registerUser("admin", "admin123", Set.of("ADMIN", "USER"));
userService.registerUser("user1", "user123", Set.of("USER"));
System.out.println("Default users created successfully");
} catch (IllegalArgumentException e) {
// Users already exist — application restarted
System.out.println("Default users already exist, skipping initialization");
}
};
}
}Summary
- User creation is the prerequisite for any authentication method in Spring Security
InMemoryUserDetailsManageris for development and testing onlyJdbcUserDetailsManagerworks for simple applications with the default schema- Custom
UserDetailsServiceis the industry standard for production applications - Always encode passwords with
BCryptPasswordEncoderbefore storing them UserDetailsService.loadUserByUsername()is the hook Spring Security uses to load users- Roles are stored with the
ROLE_prefix;roles("USER")automatically adds the prefix, whileauthorities("ROLE_USER")sets the exact value
Interview Questions
Q1: What is UserDetailsService and why is it important?
UserDetailsService is an interface with a single method: loadUserByUsername(String username). Spring Security calls this during the authentication process to retrieve user details from the configured store. Implementing this interface allows you to load users from any source: database, LDAP, external API, or memory. It is the central extension point for authentication.
Q2: What is the difference between roles() and authorities() in the User builder?
roles("USER") automatically prepends ROLE_ to produce ROLE_USER. authorities("ROLE_USER") stores the value exactly as given. When using hasRole("USER") in security config, Spring Security looks for ROLE_USER authority. So if you use authorities() without the prefix, hasRole() checks will fail.
Q3: Why should you never store passwords in plain text?
If the database is compromised, attackers immediately know every user's password. BCrypt produces a one way hash with a random salt. Even if two users have the same password, their stored hashes differ. An attacker who obtains the hash cannot reverse it to the original password without an expensive brute force attack.
Q4: What is the DelegatingPasswordEncoder and when would you use it?
DelegatingPasswordEncoder is a PasswordEncoder that stores the encoder ID in the password string (e.g., {bcrypt}$2a$10$...). It delegates encoding and verification to the correct encoder based on this ID. It is used when migrating from one password encoding scheme to another — existing users keep their old encoding while new users get the new encoding.
Q5: How does Spring Security know which UserDetailsService to use?
When you define a single UserDetailsService bean, Spring Security automatically wires it. If you have multiple implementations, you must explicitly set it in the security config using http.userDetailsService(myService) or wire it through a custom AuthenticationProvider.
Q6: What happens if loadUserByUsername() cannot find the user?
It must throw UsernameNotFoundException. Spring Security catches this exception and converts it to an AuthenticationException, which ultimately results in a 401 Unauthorized response or a redirect to the login page.
Q7: What is InMemoryUserDetailsManager and what are its limitations?
InMemoryUserDetailsManager implements UserDetailsManager (which extends UserDetailsService) and stores users in a HashMap in the JVM heap. Limitations: data is lost on restart, not suitable for distributed environments with multiple instances, not appropriate for large user bases, and does not support dynamic user creation at runtime from an external source.