Master Spring Security: Custom UserDetailsService and In‑Memory User Management

This guide walks through integrating Spring Security with Spring Boot, explains the UserDetailsServiceAutoConfiguration, demonstrates how to customize UserDetailsManager using in‑memory storage, and shows how to extend it for database‑backed user management, providing complete code examples and practical insights.

Programmer DD
Programmer DD
Programmer DD
Master Spring Security: Custom UserDetailsService and In‑Memory User Management

1. Introduction

This article continues a previous introduction to Spring Security and shows how to explore its inner workings using Spring Boot 2.x, focusing on the UserDetails and UserDetailsService components.

2. Spring Boot Integration with Spring Security

Integrating Spring Security is straightforward: add the appropriate starter dependencies to the project. The required starters are spring-boot-starter-security, spring-boot-starter-web, and optionally spring-boot-starter-actuator, lombok, and test dependencies.

<dependencies>
    <!-- actuator (optional) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- spring security starter (required) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- spring mvc servlet web (required) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- lombok (optional) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- test dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3. UserDetailsServiceAutoConfiguration

When the application starts, accessing /actuator redirects to the default login page ( /login). Spring generates a random password printed in the console, which comes from UserDetailsServiceAutoConfiguration.

The auto‑configuration class creates an InMemoryUserDetailsManager bean that holds a default UserDetails instance.

@Configuration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class })
public class UserDetailsServiceAutoConfiguration {

    private static final String NOOP_PASSWORD_PREFIX = "{noop}";
    private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
    private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

    @Bean
    @ConditionalOnMissingBean(type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
    @Lazy
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
            ObjectProvider<PasswordEncoder> passwordEncoder) {
        SecurityProperties.User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(
                User.withUsername(user.getName())
                    .password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
                    .roles(StringUtils.toStringArray(roles))
                    .build());
    }

    private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
        }
        if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
            return password;
        }
        return NOOP_PASSWORD_PREFIX + password;
    }
}

3.1 UserDetailsService

The UserDetailsService interface defines a single method:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

It loads a user by username from the underlying data source.

3.2 UserDetails

The UserDetails interface represents the core user information required by Spring Security, such as authorities, password, username, and account status flags.

Authority collection (prefixed with ROLE_)

Encoded password (prefix {noop} for plain text)

Unique username

Account non‑expired, non‑locked, credentials non‑expired, enabled flags

3.3 Custom UserDetailsManager

To replace the default in‑memory manager, a custom UserDetailsRepository is implemented that stores UserDetails objects in a Map and provides CRUD operations.

package cn.felord.spring.security;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.HashMap;
import java.util.Map;

/**
 * Proxy org.springframework.security.provisioning.UserDetailsManager all functions
 */
public class UserDetailsRepository {

    private Map<String, UserDetails> users = new HashMap<>();

    public void createUser(UserDetails user) {
        users.putIfAbsent(user.getUsername(), user);
    }

    public void updateUser(UserDetails user) {
        users.put(user.getUsername(), user);
    }

    public void deleteUser(String username) {
        users.remove(username);
    }

    public void changePassword(String oldPassword, String newPassword) {
        Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
        if (currentUser == null) {
            throw new AccessDeniedException("Can't change password as no Authentication object found in context for current user.");
        }
        String username = currentUser.getName();
        UserDetails user = users.get(username);
        if (user == null) {
            throw new IllegalStateException("Current user doesn't exist in database.");
        }
        // TODO: implement password update logic similar to InMemoryUserDetailsManager
    }

    public boolean userExists(String username) {
        return users.containsKey(username);
    }

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return users.get(username);
    }
}

The repository is exposed as a Spring bean and pre‑populated with a demo user Felordcn (password {noop}12345).

@Bean
public UserDetailsRepository userDetailsRepository() {
    UserDetailsRepository userDetailsRepository = new UserDetailsRepository();
    // Initialize a user for login
    UserDetails felordcn = User.withUsername("Felordcn")
            .password("{noop}12345")
            .authorities(AuthorityUtils.NO_AUTHORITIES)
            .build();
    userDetailsRepository.createUser(felordcn);
    return userDetailsRepository;
}

@Bean
public UserDetailsManager userDetailsManager(UserDetailsRepository userDetailsRepository) {
    return new UserDetailsManager() {
        @Override
        public void createUser(UserDetails user) {
            userDetailsRepository.createUser(user);
        }
        @Override
        public void updateUser(UserDetails user) {
            userDetailsRepository.updateUser(user);
        }
        @Override
        public void deleteUser(String username) {
            userDetailsRepository.deleteUser(username);
        }
        @Override
        public void changePassword(String oldPassword, String newPassword) {
            userDetailsRepository.changePassword(oldPassword, newPassword);
        }
        @Override
        public boolean userExists(String username) {
            return userDetailsRepository.userExists(username);
        }
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            return userDetailsRepository.loadUserByUsername(username);
        }
    };
}

The custom UserDetailsManager simply delegates all operations to the repository.

3.4 Database‑backed User Management

By replacing the in‑memory users map with a DAO (e.g., JPA or MyBatis), the same manager can operate on a persistent database.

4. Conclusion

The article explained how Spring Security loads user information via UserDetails, how the auto‑configuration creates an in‑memory manager, and how to customize the manager for in‑memory or database‑backed user stores. The full source code is available in the accompanying Git repository.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring Bootspring-securityUserDetailsServiceCustom UserDetailsManagerInMemoryUserDetailsManager
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.