Spring MVC + Spring Security (6.x)— User Details from JDBC (Security Part — 2)

This article and its corresponding codes have been updated to support Spring 6.0.9 and Spring Security 6.1.0
In this part we will extend our previous tutorial on Spring Security and discuss on how we can use JDBC based storage to store and fetch our user details. The previous source code can be found here.
If you have gone through my previous posts and done things accordingly, then I can assume you don’t need to add any dependency for this part. Things we will need to do are —
- Create a User class for persisting User Details in DB
We will create a user class for persisting our user details in Database. The source code for User class will look like below.
@Entity
@Table(name = "tbl_user")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id")
private long id;
@Column(name = "username")
private String username;
@Column(name = "password", length = 512)
private String password;
@Enumerated(EnumType.STRING)
@Column(name = "role")
private Role role;
public long getId() {
return id;
}
public void setId(long 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 Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
}
Since we are using an Enum type Role, therefore, we will create a Role enum class.
public enum Role {
ROLE_USER, ROLE_ADMIN
}
For database backed roles, we must have to add ROLE_ prefix which is fixed by Spring Security. Therefore our role names are ROLE_USER, ROLE_ADMIN.
2. A UserRepository Interface
We need to create a UserRepository Interface which will help us to do all interactions with DB.
@Repository
@Transactional
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
3. A UserService class implementing UserDetailsService interface
Now we will create a UserService class which will implement UserDetailsService interface. UserDetailsService is an interface provided by Spring Security. It has only one method loadUserByUsername(String username). In Spring Security configuration we will have to pass a instance of this interface implementation, therefore we are doing so.
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
var userFromDb = userRepository.findByUsername(s)
.orElseThrow(() -> new UsernameNotFoundException("No user found with this email address."));
List<GrantedAuthority> authorities = new java.util.ArrayList<>(Collections.emptyList());
authorities.add((GrantedAuthority) () -> userFromDb.getRole().name());
return new User(userFromDb.getUsername(), userFromDb.getPassword(), authorities);
}
}
The returning User class is from Spring Security. Since we did not implement UserDetails interface from Spring Security, therefore we are creating an instance of Spring Security provided User class.
4. Creating a Bean of PasswordEncoder
Now we will create a bean of PasswordEncoder class which will help us to encode passwords. We will write it in some RootConfig class to remain safe from having circular dependency.
We could do it in a separate @Configuration annotated class but we would have to make sure it does not get a circular dependency injection.
@Bean(name = "passwordEncoder")
PasswordEncoder BCPasswordEncoder(){
return new BCryptPasswordEncoder(11);
}
5. Re-writing our configuration
Now, since our things are in place, we will have to re-write our SecurityConfig class. We will replace our old in memory configuration code with our following new code. In the new code, a method will inject the instances of AuthenticationManagerBuilder, UserService and PasswordEncoder beans in a method parameter. The purpose of this method will be allowing the AuthenticationManagerBuilder instance to use UserService instance (UserService is the implementation of UserDetailsService, provided by Spring Security. Spring Security will look for an instance of any implementation of UserDetailsService) to read the users from DB. The PasswordEncoder instance will be used to match the password in the DB with the provided password via form.
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, UserService userService, PasswordEncoder passwordEncoder) throws Exception {
//##################### Custom UserDetailsService Authentication #####################
auth.userDetailsService(userService).passwordEncoder(passwordEncoder);
}
6. Dummy Users registration for both ROLE_ADMIN and ROLE_USER
Let’s do some [dirty but understandable] code in controller for initializing our users. In our RootController class, we will write the following method.
private void generateUsers() {
if (userRepository.findByUsername("admin").isEmpty()) {
var user = new User();
user.setUsername("admin");
user.setPassword(passwordEncoder.encode("secret"));
user.setRole(Role.ROLE_ADMIN);
userRepository.save(user);
}
if (userRepository.findByUsername("user").isEmpty()) {
var user = new User();
user.setUsername("user");
user.setPassword(passwordEncoder.encode("secret"));
user.setRole(Role.ROLE_USER);
userRepository.save(user);
}
}
We will call the above method from login() method.
@GetMapping("/login")
public String login(Model model, @RequestParam(name="error", required = false) String error) {
generateUsers();
model.addAttribute("error", error);
return "auth/login";
}
Don’t forget to inject UserRepository and PasswordEncoder in RootController class, since our newly added generateUser() method uses them.
I have injected the UserRepository and PasswordEncoder beans in the constructor injection.
public RootController(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
Now run your application. If you have done everything properly, It will work.
The complete source code can be found here.