February 16th, 2023
UserDetailsService : Loading UserDetails from database

Resources

Pom.xml

				
					<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.8</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>userdetailService-db</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>userdetailService-db</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<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>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</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-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

				
			

application.properties

				
					spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
 
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto= update

spring.h2.console.enabled=true
# default path: h2-console
spring.h2.console.path=/h2-ui
				
			

Database entities

In order to load user information from the database, we need to use spring JDBC or spring JPA. For the sake of completeness, I’m using spring JPA and here is a simple UserAccount and UserRole entities.

				
					package com.example.demo.model;

import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

import lombok.Data;

@Entity
@Data
public class UserAccount {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	
	@Column(unique = true)
	private String username;
	
	private String password;
	
	private boolean active;
	
	@OneToMany(cascade = CascadeType.ALL)
	private List<UserRole> userRoles;
}

				
			
				
					package com.example.demo.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

import lombok.Data;

@Entity
@Data
public class UserRole {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;
	
	private String role;
	
	@ManyToOne
	private UserAccount userAccount;
}

				
			
  1. Here the UserRole is to show how a single user can have multiple roles (@OneToMany).
  2. The username column is marked as unique due to the nature of how usernames should be. However, it’s up to you how you want to design the database entries.
  3. I’m using Lombok hence there are no getters and setters.

With the entities ready, let’s write the necessary repository methods for our CustomUserDetailService. The following definition would return an UserAccount entity based on the username.

				
					package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.example.demo.model.UserAccount;

@Repository
public interface UserAccountRepository extends JpaRepository<UserAccount, Integer> {
	UserAccount findByUsername(String username);
}
				
			

UserDetails

The UserDetailsService service interface is supposed to return an implementation of org.springframework.security.core.userdetails.UserDetails. So first we need to define a CustomUserDetails class backed by an UserAccount. Here is how I implemented them. However, it is up to you to implement this class differently if you have to.

				
					package com.example.demo.service;

import java.util.Collection;
import java.util.Collections;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.example.demo.model.UserAccount;

public class CustomUserDetails implements UserDetails {

	private final UserAccount userAccount;
	
	public CustomUserDetails(UserAccount userAccount) {
		this.userAccount = userAccount;
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return Collections.singletonList(new GrantedAuthority() {
			
			@Override
			public String getAuthority() {
				return "USER";
			}
		});
	}

	@Override
	public String getPassword() {
		return userAccount.getPassword();
	}

	@Override
	public String getUsername() {
		return userAccount.getUsername();
	}

	@Override
	public boolean isAccountNonExpired() {
		return userAccount.isActive();
	}

	@Override
	public boolean isAccountNonLocked() {
		return userAccount.isActive();
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return userAccount.isActive();
	}

	@Override
	public boolean isEnabled() {
		return userAccount.isActive();
	}

}

				
			

The getAuthorities() method of UserDetails needs a list of GrantedAuthority. For now, I have hardcoded it to return only USER the role. Also, I have written a getUserAccount() so that I can use this to get hold of the current user entity.

Loading user details from the database

With all the above set, All we need is UserDetailService implementation. As we have already established our database entities and repositories, let’s write our performance and mark it a bean with the help of @Component annotation.

				
					package com.example.demo.service;

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.Component;

import com.example.demo.model.UserAccount;
import com.example.demo.repository.UserAccountRepository;

@Component
public class DatabaseUserDetailsService implements UserDetailsService {

	private final UserAccountRepository accountRepository;
	
	public DatabaseUserDetailsService(UserAccountRepository accountRepository) {
		this.accountRepository = accountRepository;
	}
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		UserAccount userAccount = accountRepository.findByUsername(username);
		if(userAccount == null) {
			throw new UsernameNotFoundException("User with username ["+username+"] not found");
		}
		return new CustomUserDetails(userAccount);
	}
}
				
			

This is as simple as it can get. The contract for this method is that if the system is not able to find a user for a given username, the method should throw a UsernameNotFoundException message. Once the method gets a UserAccount record, It is converted into CustomUserDetails and presented in a security context.

				
					package com.example.demo.controller;

import java.util.Arrays;

import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.model.UserAccount;
import com.example.demo.model.UserRole;
import com.example.demo.repository.UserAccountRepository;

@RestController
public class UserController {

	private UserAccountRepository userAccountRepository;
	private PasswordEncoder passwordEncoder;
	
	public UserController(UserAccountRepository userAccountRepository, PasswordEncoder passwordEncoder) {
        this.userAccountRepository = userAccountRepository;
        this.passwordEncoder = passwordEncoder;
    }
	
	@GetMapping("/")
	public String greeting() {
		return "Hey there ";
	}
	
	@GetMapping("/login")
	public String login(Authentication auth) {
		return "Welcome there "+ auth.getName();
	}
	
	@PostMapping("/register")
	public UserAccount register(@RequestParam("username") String username, @RequestParam("password") String password) {
		UserAccount userAccount = new UserAccount();
		userAccount.setUsername(username);
		userAccount.setPassword(passwordEncoder.encode(password));
		UserRole role = new UserRole();
		role.setRole("USER");
		userAccount.setUserRoles(Arrays.asList(role));
		userAccount.setActive(true);
		return userAccountRepository.save(userAccount);
	}
}

				
			

To make sure register.html and /register accessible without requiring login, exclude them from web security.

				
					package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.authorizeRequests()
				.antMatchers("/").permitAll()
				.anyRequest().authenticated()
				.and()
			.httpBasic()
				.and()
			.logout()
				.permitAll();
	}
	
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring()
			.antMatchers(HttpMethod.POST, "/register")
			.antMatchers("/h2-ui/**");
	}
	
	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}

				
			

The Result

With all the above in place, let’s test by registering a user and logging in using the same application.

 

Register new user

 

 

 

Related Tutorials

4 responses to “UserDetailsService : Loading UserDetails from database”

  1. Really many of useful knowledge.

  2. Lovely postings. With thanks!

  3. Valuable content Appreciate it!

  4. Wonderful material, Many thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *