Many applications require users to register and sign in. They keep track of user details like usernames, passwords, and roles in a permanent storage system, such as a database.
In this topic, you will learn how to connect a custom user storage system to Spring Security for the authentication process. When a user tries to log in with their username and password, Spring Security checks if the user exists in the storage system and their login details are correct. Depending on the results, Spring Security either grants or denies access. This knowledge will help you connect any user storage system, be it an in-memory storage, a file, a database, or an external server.
Initial setup
In our example, we will use basic authorization and the H2 database to store user details persistently.
Here are the necessary dependencies for this demo Spring Boot project:
Maven
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>Gradle (Groovy)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
runtimeOnly 'com.h2database:h2'
}Gradle (Kotlin)
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
runtimeOnly("com.h2database:h2")
}First, we'll create a controller with two endpoints. The first endpoint, GET /test, is only accessible to authenticated users with the ROLE_USER role. We can check the endpoint to see if our application is working correctly. The second endpoint, POST /register, is open to everyone. This endpoint will accept user registration data like a username, a password, and a role.
Here's the code for our controller:
Java
import org.springframework.web.bind.annotation.*;
@RestController
public class DemoController {
@PostMapping(path = "/register")
public String register(@RequestBody RegistrationRequest request) {
return "New user successfully registered";
}
@GetMapping(path = "/test")
public String test() {
return "Access to '/test' granted";
}
record RegistrationRequest(String username, String password, String authority) { }
}Kotlin
import org.springframework.web.bind.annotation.*
@RestController
class DemoController {
@PostMapping("/register")
fun register(@RequestBody request: RegistrationRequest): String {
return "New user successfully registered"
}
@GetMapping("/test")
fun test(): String {
return "Access to '/test' granted"
}
data class RegistrationRequest(val username: String, val password: String, val authority: String)
}Next, we'll handle the security configuration. The code below shows our security settings. We specify that the /test endpoint requires the ROLE_USER role, the /register endpoint is open to everyone, we provide a password encoder bean and define some additional settings.
Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.httpBasic(Customizer.withDefaults()) // Default Basic auth config
.csrf(configurer -> configurer.disable()) // for POST requests via Postman
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/register").permitAll()
.requestMatchers(HttpMethod.GET, "/test").hasRole("USER")
.anyRequest().denyAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
@Configuration
class SecurityConfig {
@Bean
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.httpBasic(Customizer.withDefaults()) // Default Basic auth config
.csrf { it.disable() } // for POST requests via Postman
.authorizeHttpRequests { auth ->
auth
.requestMatchers(HttpMethod.POST, "/register").permitAll()
.requestMatchers(HttpMethod.GET, "/test").hasRole("USER")
.anyRequest().denyAll()
}
return http.build()
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
}The code above is part of our security configuration. We'll set up authentication later.
We mentioned earlier that we'll store user details in a relational database. This means we need an entity to describe the user. But first, we need to understand what data Spring Security needs to authenticate and authorize a user.
UserDetails
Spring Security uses an interface called UserDetails to manage user information. This interface includes seven methods that we need to implement in our user class. Here is the interface:
package org.springframework.security.core.userdetails;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}The getAuthorities() method returns a collection of roles and authorities given to the user. You can use the SimpleGrantedAuthority class to convert a string describing a role or an authority into a GrantedAuthority object:
Java
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_USER");Kotlin
val grantedAuthority: GrantedAuthority = SimpleGrantedAuthority("ROLE_USER")The getPassword() and getUsername() methods return the user's password and username, respectively.
The remaining methods are used to manage the user's account status. If your application doesn't require these features, you can make these methods always return true to indicate that the user is always active.
Next, let's create an entity class to store user information in the database:
Java
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class AppUser {
@Id
@GeneratedValue
private Integer id;
private String username;
private String password;
private String authority;
public Integer getId() {
return id;
}
public void setId(Integer 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 String getAuthority() {
return authority;
}
public void setAuthority(String role) {
this.authority = role;
}
}Kotlin
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
@Entity
class AppUser(
@Id @GeneratedValue var id: Int? = null,
var username: String? = null,
var password: String? = null,
var authority: String? = null
)This is just a simple example; you can model user-related data differently.
We also need a repository to handle database operations:
Java
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface AppUserRepository extends CrudRepository<AppUser, Integer> {
Optional<AppUser> findAppUserByUsername(String username);
}Kotlin
import org.springframework.data.repository.CrudRepository
interface AppUserRepository : CrudRepository<AppUser, Int> {
fun findAppUserByUsername(username: String): AppUser?
}Finally, we will create an adapter class to connect our user entity with the UserDetails interface:
Java
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class AppUserAdapter implements UserDetails {
private final AppUser user;
public AppUserAdapter(AppUser user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(user.getAuthority()));
}
@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
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class AppUserAdapter(private val user: AppUser) : UserDetails {
override fun getAuthorities(): Collection<GrantedAuthority> {
return listOf(SimpleGrantedAuthority(user.authority))
}
override fun getPassword(): String = requireNotNull(user.password)
override fun getUsername(): String = requireNotNull(user.username)
override fun isAccountNonExpired(): Boolean = true
override fun isAccountNonLocked(): Boolean = true
override fun isCredentialsNonExpired(): Boolean = true
override fun isEnabled(): Boolean = true
}
As an implementation of the UserDetails interface, you can also use the User class provided by Spring Security in the org.springframework.security.core.userdetails package. This class has a builder to help make your code more concise, as you could have seen in the Authentication and Authorization topics.
Next, let's implement the UserDetailsService interface.
UserDetailsService
Spring Security uses this interface to get user data from the database:
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}When a user tries to log in using a login and a password, Spring Security calls the loadUserByUsername method to get the user's data. If the username doesn't exist, the method should throw a UsernameNotFoundException. Here is an implementation example:
Java
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;
@Service
public class AppUserDetailsServiceImpl implements UserDetailsService {
private final AppUserRepository repository;
public AppUserDetailsServiceImpl(AppUserRepository repository) {
this.repository = repository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = repository
.findAppUserByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Not found"));
return new AppUserAdapter(user);
}
}Kotlin
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
@Service
class AppUserDetailsImpl(private val repository: AppUserRepository) : UserDetailsService {
@Throws(UsernameNotFoundException::class)
override fun loadUserByUsername(username: String): UserDetails {
val user = repository.findAppUserByUsername(username)
?: throw UsernameNotFoundException("Not found")
return AppUserAdapter(user)
}
}In this class, we use the AppUserRepository to get user data from the database. We don't need to check the password or roles here because Spring Security handles that on its own. Our job is just to return the UserDetails object that contains the user's data. We create an instance of our implementation of the UserDetails interface and return it to the caller.
User registration logic
Before we can test our program, we need to incorporate the logic for registering users. We'll do it in our DemoController:
Java
@RestController
public class DemoController {
private final AppUserRepository repository;
private final PasswordEncoder passwordEncoder;
public DemoController(AppUserRepository repository,
PasswordEncoder passwordEncoder) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
}
@PostMapping(path = "/register")
public String register(@RequestBody RegistrationRequest request) {
var user = new AppUser();
user.setUsername(request.username());
user.setPassword(passwordEncoder.encode(request.password()));
user.setAuthority(request.authority());
repository.save(user);
return "New user successfully registered";
}
// other method(s)
}Kotlin
@RestController
class DemoController(
private val repository: AppUserRepository,
private val passwordEncoder: PasswordEncoder)
{
@PostMapping("/register")
fun register(@RequestBody request: RegistrationRequest): String {
val user = AppUser()
user.username = request.username
user.password = passwordEncoder.encode(request.password)
user.authority = request.authority
repository.save(user)
return "New user successfully registered"
}
// other method(s)
}We've injected AppUserRepository and PasswordEncoder into the controller. In the register method, we take user data from the request body and store it in the entity. Remember to encode the password before saving it!
Now we are ready to run our program and test it using Postman.
Testing the application
First, we populate the user store with data using the public POST /register endpoint. We'll create two users. The first user will be user1 with the password pass1 and the role ROLE_USER. The second user will be user2, with the password pass2 and the role ROLE_ADMIN.
Let's register the users:
Next, let's try to access the /test endpoint protected with basic auth using the users' usernames and passwords:
As shown, the first user can access the endpoint, and the status code is 200 OK. The second user received a 403 FORBIDDEN status, meaning the authentication was successful, but the user doesn't have the correct role to access the endpoint. This indicates that our registration endpoint and UserDetails and UserDetailsService are working properly.
What happens during authentication when a username and password are provided? Simply put, the username is passed to the loadUserByUsername method of the UserDetailsService bean. This method checks if the user exists in the user store. If the user is not found, a UsernameNotFoundException is thrown, meaning authentication failed, and we get a 401 UNAUTHORIZED status code. If the user is found, the user data is retrieved, converted to UserDetails, and returned to the component handling authentication. Then, the component uses PasswordEncoder to check if the input password matches the stored one, and the roles are compared. If everything checks out, access is granted.
If you're using the H2 database in your app and want to use the H2 console, you need to disable CSRF protection and X-Frame-Options to unblock it. You can do this by calling .csrf(cfg -> cfg.disable()).headers(cfg -> cfg.frameOptions().disable()) on the HttpSecurity object. Also, ensure that the H2 console URL isn't blocked by Spring Security.
Conclusion
In this topic, you learned how to connect a custom user store to Spring Security by implementing the UserDetails and UserDetailsService interfaces. UserDetails holds core user information, while UserDetailsService retrieves user-related data from the user store. We've also created a simple registration endpoint to add data to the user store and successfully tested the program.
You can use any custom user store in a similar way, whether you're storing user data in memory, a non-relational database, or a file system. Feel free to experiment with this.