Here's a recap of Spring Security architecture:
When Spring Security is enabled, a client's request undergoes interception by a series of filters before it reaches the controller for further processing. The AuthenticationFilter takes charge of handling authentication requests by delegating them to the AuthenticationManager.
The AuthenticationManager, in turn, relies on an AuthenticationProvider to carry out the authentication process. The key role of the AuthenticationProvider is to interact with UserDetailsService, which primarily handles user management responsibilities.
The principal responsibility for UserDetailsService is loading a user based on their username within the cache or underlying storage system. The PasswordEncoder interface is used to perform a one-way transformation of a password to let the password be stored securely. In this topic, we will learn about UserDetailsService interface and we will explore various implementations of UserDetailsManager, including the InMemory and database-backed Jdbc options.
Project setup
Create a new spring project with the following dependency:
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>Gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'Gradle(Kotlin)
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")Default UserDetailsService
Spring Security is often configured with default credentials in memory with randomly generated UUID-based passwords. This is done only by adding Spring Security dependency. Now let's run the project: you will see a random password generated in the console, like this:
We can go directly to http://localhost:8080/login, use the generated password, try to log in, and note that the default username is user.
UserDetailsService
If you look back at the diagram for Spring Security architecture, you will see that AuthenticationProvider only needs a user. So, if we look at the UserDetailsService source code, we will see a contract that has only one method to load the user by name:
Java
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}Kotlin
interface UserDetailsService {
@Throws(UsernameNotFoundException::class)
fun loadUserByUsername(username: String): UserDetails
}The UsernameNotFoundException exception will be thrown in the case the requested user details can't be fetched in the loadUserByUsername method of the UserDetailsService interface. This exception might occur because the user is not found in the database or because of the database's incorrect configuration, which is instead used to give the user good feedback or discover underlying issues for debugging.
We should declare and implement the UserDetailsService interface when customizing user authentication. Here's an example:
Java
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
return new SecurityUser(user);
}
}Kotlin
@Service
class CustomUserDetailsService
@Autowired constructor(
private val userRepository: UserRepository
) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
val user = userRepository.findByUsername(username)
?: throw UsernameNotFoundException("User not found with username: $username")
return SecurityUser(user)
}
}And here's how we can define UserRepository:
Java
@Repository
public class UserRepository {
private final Map<String, UserProfile> userMap = new ConcurrentHashMap<>();
public void addUser(UserProfile user) {
userMap.put(user.getUsername(), user);
}
public UserProfile findByUsername(String username) {
return userMap.get(username);
}
@PostConstruct
public void init() {
addUser(new UserProfile(1L, "username", "Aa123456789", List.of("ROLE_USER")));
}
}Kotlin
@Repository
class UserRepository {
private val userMap: MutableMap<String, UserProfile> = ConcurrentHashMap()
fun addUser(user: UserProfile) {
userMap[user.username] = user
}
fun findByUsername(username: String): UserProfile? {
return userMap[username]
}
@PostConstruct
fun init() {
addUser(UserProfile(1L, "username", "Aa123456789", listOf("ROLE_USER")))
}
}The init() method annotated with @PostConstruct is executed after the bean has been constructed and all dependency injections are complete. We did this to add userMap representing user storage here.
Of course, you can customize UserRepository as you want, for example, making your repository extend JpaRepository, CrudRepository or MongoRepository.
And here's the SecurityUser:
Java
public class SecurityUser implements UserDetails {
private final UserProfile user;
public SecurityUser(UserProfile user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}Kotlin
class SecurityUser(private val user: UserProfile) : UserDetails {
override fun getAuthorities(): Collection<GrantedAuthority> {
return user.roles.map { SimpleGrantedAuthority(it) }
}
override fun getPassword(): String {
return user.password
}
override fun getUsername(): String {
return user.username
}
override fun isAccountNonExpired(): Boolean = true
// Implement other UserDetails methods with similar default implementations
override fun isAccountNonLocked(): Boolean = true
override fun isCredentialsNonExpired(): Boolean = true
override fun isEnabled(): Boolean = true
}Here we injected UserProfile responsible for user's profile information in the SecurityUser, which focuses on implementing the security-related functionality provided by the UserDetails interface.
The UserProfile will look something like this:
Java
public class UserProfile {
private final Long id;
private final String username;
private final String password;
private final List<String> roles;
public UserProfile(Long id, String username, String password, List<String> roles) {
this.id = id;
this.username = username;
this.password = password;
this.roles = roles;
}
public Long getId() {
return id;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public List<String> getRoles() {
return roles;
}
}Kotlin
data class UserProfile(
val id: Long,
val username: String,
val password: String,
val roles: List<String>
)SecurityUser and UserProfile could be one class, but separating them is better as this gives each class a single responsibility.
Note that UserDetailsService is always associated with a PasswordEncoder that encodes a supplied password and verifies if the password matches an existing encoding. When we replace the default implementation of the UserDetailsService, we must also specify a PasswordEncoder. Here's how we do it:
Java
@Configuration
public class AppConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}Kotlin
@Configuration
class AppConfig {
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance()
}
}We can now go to http://localhost:8080/login and try to log in with the username and password we added.
Simply put, UserDetailsService fetches user details by the provided username. That's it!
Looks good, doesn't it? But in most apps, not only do we need to load users, but we also need to manage them: create a new user, edit or delete one. That means we need another contract, and here is where UserDetailsManager comes to the rescue.
UserDetailsManager
Here the UserDetailsManager interface :
Java
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}Kotlin
interface UserDetailsManager : UserDetailsService {
fun createUser(user: UserDetails)
fun updateUser(user: UserDetails)
fun deleteUser(username: String)
fun changePassword(oldPassword: String, newPassword: String)
fun userExists(username: String): Boolean
}This interface extends the UserDetailsService interface. UserDetailsManager has five methods: createUser, updateUser, deleteUser, changePassword, userExists, and the inherited method loadUserByUsername.
In-memory UserDetailsService
Starting from here, remove the previous code examples for UserDetailsService as it won't work with the coming code examples using built-in implementations
Now, let's create a custom UserDetailsService :
Java
@Configuration
public class AppConfig {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager customUserDetailsService = new InMemoryUserDetailsManager();
UserDetails user = User.withUsername("username")
.password("Aa12345678")
.authorities("read")
.build();
customUserDetailsService.createUser(user);
return customUserDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}Kotlin
@Configuration
class AppConfig {
@Bean
fun userDetailsService(): UserDetailsService {
val customUserDetailsService = InMemoryUserDetailsManager()
val user: UserDetails = User.withUsername("username")
.password("Aa12345678")
.authorities("read")
.build()
customUserDetailsService.createUser(user)
return customUserDetailsService
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance()
}
}Now, we replaced the default configuration by adding a bean in the context to UserDetailsService. We also used the InMemoryUserDetailsManager class that implements UserDetailsManager, which extends UserDetailsService, as mentioned before.
InMemoryUserDetailsManager is a non-persistent implementation of UserDetailsManager, which is backed by an in-memory map. It's mainly intended for testing and demonstration purposes, where a full-blown persistent system isn't required.
The customUserDetailsService.createUser(user); is now relevant as we know now where createUser came from. Its inherited method comes from UserDetailsService.
Finally, let's run the project and log in with the username and password we added.
Database-backed UserDetailsService
We need to add two more dependencies to store a user in the database. Let's use the H2 database now:
Maven
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>Gradle
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'Gradle(Kotlin)
implementation("org.springframework.boot:spring-boot-starter-jdbc")
runtimeOnly("com.h2database:h2")Let's create the following:
Java
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
.build();
}
@Bean
public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
UserDetails user = User.withUsername("username")
.password("Aa12345678")
.roles("ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
return users;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}Kotlin
@Configuration
class AppConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
.build()
}
@Bean
fun jdbcUserDetailsService(dataSource: DataSource): UserDetailsService {
val user: UserDetails = User
.withUsername("username")
.password("Aa12345678")
.roles("ADMIN")
.build()
val users = JdbcUserDetailsManager(dataSource)
users.createUser(user)
return users
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance()
}
}For the DataSource:
We created the
DataSourcebean.new EmbeddedDatabaseBuilder(): this creates a new instance ofEmbeddedDatabaseBuilder, which is a builder class provided by Spring for configuring and creating embedded databases..setType(EmbeddedDatabaseType.H2): this line sets the type of the embedded database to H2..addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION): this line adds a SQL script to be executed when the database is created. The script is obtained from theJdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATIONproperty. Of course, we can customize this, but it will require a lot of code writing.
The rest of the code is the same as before, but here we used JdbcUserDetailsManager which is the same as UserDetailsManager but used to manage users in a database. We also injected the datasource bean we created into the JdbcUserDetailsManager.
In fact, we can define DataSource with the autoconfiguration, but we will need Spring Data JPA Dependency:
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>Gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'Gradle(Kotlin)
implementation("org.springframework.boot:spring-boot-starter-data-jpa")Now, let's rewrite the previous example with injecting DataSource via constructor injection:
Java
@Configuration
public class AppConfig {
private final DataSource dataSource;
public AppConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public UserDetailsService jdbcUserDetailsService() {
UserDetails user = User.withUsername("username")
.password("Aa12345678")
.roles("ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
return users;
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}Kotlin
@Configuration
class AppConfig(private val dataSource: DataSource) {
@Bean
fun jdbcUserDetailsService(): UserDetailsService {
val user: UserDetails = User
.withUsername("username")
.password("Aa12345678")
.roles("ADMIN")
.build()
val users = JdbcUserDetailsManager(dataSource)
users.createUser(user)
return users
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance()
}
}Let's try running the app. BOOM — BeanCreationException. An error occurred while creating the bean called jdbcUserDetailsService with a message: "PreparedStatementCallback; bad SQL grammar [insert into users (username, password, enabled) values (?,?,?)]".
We now need to define the tables first for the users and authorities. Put this file schema.sql in the src/main/resources with the following:
-- Create the users table
CREATE TABLE users (
username VARCHAR(50) PRIMARY KEY,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL
);
-- Create the authorities table (for roles)
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users(username)
);
-- Define an index for the username column for faster lookups
CREATE UNIQUE INDEX ix_auth_username ON authorities(username, authority);Now this file will executed during the application startup to create the schema. Let's rerun the app and check it again.
Custom DataSource
We can use another database like PostgreSQL and create the tables using Hibernate or manually. To do this, we need to add the following dependency, which allows Java programs to connect to the PostgreSQL database (or another one of your choice):
Maven
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>Gradle
runtimeOnly 'org.postgresql:postgresql'Gradle (Kotlin)
runtimeOnly("org.postgresql:postgresql")Now we create the database, name it, and create the following tables:
CREATE TABLE IF NOT EXISTS users (
username VARCHAR(50) PRIMARY KEY,
password VARCHAR(255) NOT NULL,
enabled BOOLEAN NOT NULL
);
CREATE TABLE IF NOT EXISTS authorities (
username VARCHAR(50),
authority VARCHAR(50),
CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users(username)
);Our code should look like this:
Java
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.postgresql.Driver");
dataSource.setUrl("jdbc:postgresql://localhost:5432/database-name");
dataSource.setUsername("your-username");
dataSource.setPassword("your-password");
return dataSource;
}
@Bean
public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}Kotlin
@Configuration
class AppConfig {
@Bean
fun dataSource(): DataSource {
return DriverManagerDataSource().apply {
setDriverClassName("org.postgresql.Driver")
url = "jdbc:postgresql://localhost:5432/database-name"
username = "your-username"
password = "your-password"
}
}
@Bean
fun jdbcUserDetailsService(dataSource: DataSource): UserDetailsService {
return JdbcUserDetailsManager(dataSource)
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance()
}
}We created a DataSource bean using DriverManagerDataSource and configured it.
You can achieve the same result by writing the database configuration in the properties file.
That's it! Now we only need to create a user. We could do this by using REST API or GraphQL API, but in this topic, we will create a user using the following SQL:
INSERT INTO users (username, password, enabled) VALUES ('username', 'Aa12345678', true);
INSERT INTO authorities (username, authority) VALUES ('username', 'ADMIN');Now you can go to http://localhost:8080/login and try to log in. The app will store the user in the database even if you rerun the app. Also, you can use any other database in your use case.
Do we use the implementation provided by Spring or implement UserDetailsService from scratch? The rule of thumb is to follow the DRY (Don't Repeat Yourself) principle: if we find an implementation that suits our needs, let's use it just like we did with JdbcUserDetailsManager!
Conclusion
In this topic, you learned about UserDetailsService and its role in the Spring Security architecture as a universal interface for fetching user details. You also learned about the flexibility of UserDetailsService and UserDetailsManager interface as an extension of UserDetailsService. We also had a look at the available implementations of UserDetailsManager, such as InMemory and Jdbc. The UserDetailsService interface can have any source of user details data, including any suitable database, file system, and more.