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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
