Resources
Pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.7.8
com.example
userdetailService-db
0.0.1-SNAPSHOT
userdetailService-db
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
com.h2database
h2
runtime
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
org.springframework.boot
spring-boot-maven-plugin
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 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;
}
- Here the
UserRole
is to show how a single user can have multiple roles (@OneToMany). - 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.
- 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 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
Really many of useful knowledge.
Lovely postings. With thanks!
Valuable content Appreciate it!
Wonderful material, Many thanks!