Skip to content

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:

  1. In memory (for development and testing)
  2. Database storage via JDBC (for production)
  3. 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 schema

Approach 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?

ApproachWhen to UseProsCons
InMemoryUserDetailsManagerDev, testing, demosSimple, no DB neededNot persistent, not scalable
JdbcUserDetailsManagerSimple apps with fixed schemaBuilt in, no custom codeRigid schema, limited flexibility
Custom UserDetailsServiceProduction, real appsFull control, any schemaMore 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
  • InMemoryUserDetailsManager is for development and testing only
  • JdbcUserDetailsManager works for simple applications with the default schema
  • Custom UserDetailsService is the industry standard for production applications
  • Always encode passwords with BCryptPasswordEncoder before 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, while authorities("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.