Photo by ThisisEngineering on Unsplash
PART 4: Integrate the database with Spring Security.
Spring Security Basics: Implementing Authentication and Authorization
Up to this point, we have used the default user provided by Spring Security to log in to the application. In the previous sections, we added some sample users to the application_users
table. Moving forward, we will use this table for logging in to the application. To achieve this, we need to configure Spring Security to recognize that the details of the application users are stored in the application_users
table. This will enable Spring Security to retrieve and verify user credentials during authentication and authorization.
For that let's do the following steps:
Add the password encoder bean
Update the plain text password to encrypted password
Configure user details service
Configure the authentication provider
Add the password encoder bean
In the SecurityConfig
class add a bean of type PasswordEncoder
package com.gintophilip.springauth.web;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity securityConfig) throws Exception {
return securityConfig
.authorizeHttpRequests(auth->
auth.requestMatchers("/api/hello")
.permitAll()
.requestMatchers("/api/admin").hasRole("ADMIN")
.anyRequest().authenticated()
).formLogin(Customizer.withDefaults())
.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
The BCryptPasswordEncoder
is the preferred method for encoding passwords.
Update the plain text password to encrypted password.
While creating the sample users, the passwords were stored as plain text. In this step, let's update the passwords to an encrypted form.
Drop the
user_roles
tableDrop the
roles
tableDrop the
application_user
tableAdd the
PasswordEncoder
class dependency inDataBaseUtilityRunner
Encode the password
Drop theuser_roles
table
spring_access_db=# drop table user_roles;
Drop theroles
table
spring_access_db=# drop table roles;
Drop theapplication_user
table
spring_access_db=# drop table application_user;
And also, do the following in DataBaseUtilityRunner
class.
update the lines
user1.setPassword("123456");
adminUser.setPassword("12345");
as follows.
user1.setPassword(passwordEncoder.encode("123456"));
adminUser.setPassword(passwordEncoder.encode("12345"));
The DataBaseUtilityRunner
after modification is as follows,
package com.gintophilip.springauth;
import com.gintophilip.springauth.entities.ApplicationUser;
import com.gintophilip.springauth.entities.Roles;
import com.gintophilip.springauth.repository.RoleRepository;
import com.gintophilip.springauth.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class DataBaseUtilityRunner implements CommandLineRunner {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserRepository usersRepository;
@Autowired
RoleRepository rolesRepository;
@Override
public void run(String... args) throws Exception {
try {
Roles userRole = new Roles();
userRole.setRoleName("USER");
Roles adminRole = new Roles();
adminRole.setRoleName("ADMIN");
rolesRepository.save(userRole);
rolesRepository.save(adminRole);
ApplicationUser user1 = new ApplicationUser();
user1.setFirstName("John");
user1.setEmail("john@test.com");
user1.setPassword(passwordEncoder.encode("123456"));
user1.setRole(userRole);
ApplicationUser adminUser = new ApplicationUser();
adminUser.setFirstName("sam");
adminUser.setEmail("sam@test.com");
adminUser.setPassword(passwordEncoder.encode("12345"));
adminUser.setRole(adminRole);
usersRepository.save(user1);
usersRepository.save(adminUser);
}catch (Exception exception){
}
}
}
Run the application and query the application_user
table. You will see the passwords are encoded now instead of plain text.
spring_access_db=# select * from application_user;
id | email | first_name | last_name | password
-----+---------------+------------+-----------+--------------------------------------------------------------
102 | john@test.com | John | | $2a$10$IBP9g8pOvNCvkEc7/EG6TO3j6gh49QMuO6uuw9Dd/P9dPRi5mxbiAsnG
103 | sam@test.com | sam | | $2a$10$WxM0l4qnjbYn1Vkgmrbte.hgYqPyHLm/y.9IGvEoiRkrL8.h47QKu
(2 rows)
Configure user details service
For making spring security to use the database for authentication and authorization a user details service needs to be implemented. This is nothing but a class which implements the interface UserDetailsService
.
This interface has a method named loadUserByUsername
which accepts a string parameter and returns a UserDetails
object.
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Create a class named DatabaseUserDetailsService.java
which implements the UserDetailsService
interface.
public class DatabaseUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
In the overridden method we do the following
Fetch the user from database based on the parameter username.
Retrieve the roles associated with the user
Create an instance of class
User
with the user details and the associated rolesReturn the
User
object
package com.gintophilip.springauth.service;
import com.gintophilip.springauth.entities.ApplicationUser;
import com.gintophilip.springauth.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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;
import java.util.Collections;
@Service
public class DatabaseUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ApplicationUser user = userRepository.findByEmail(username); //fetch user from db
if(user == null){
throw new UsernameNotFoundException("User Not found");
}
//Retrieve user roles
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_"+user.getRole().getRoleName());
//create User object
User applicationUser = new User(user.getEmail(),user.getPassword(), Collections.singleton(authority));
return applicationUser;
}
}
User
class implements the UserDetails
interface. Spring utilizes the UserDetails
to generate an Authentication object. This Authentication object indicates whether the user is authenticated.Next we need to configure an authentication provider.
Configure the authentication provider
For Spring Security to utilize the DatabaseUserDetailsService
for retrieving user details, it must be linked with an authentication provider. For that we will create a bean of type DaoAuthenticationProvider
in the SecurityConfig
and bind it with DatabaseUserDetailsService
within the SecurityConfig
.
Bind the DatabaseUserDetailsService
@Autowired
DatabaseUserDetailsService databaseUserDetailsService;
Next, create an instance of DaoAuthenticationProvider
which is an implementation of AUthenticationProvider
given by Spring security. Then,
Link the
databaseUserDetailsService
Link the
passwordEncoder
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
//set the user details object
authenticationProvider.setUserDetailsService(databaseUserDetailsService);
//set the password encoder
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
After this the SecurityConfig
looks like below.
package com.gintophilip.springauth.web;
import com.gintophilip.springauth.service.DatabaseUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
DatabaseUserDetailsService databaseUserDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity securityConfig) throws Exception {
return securityConfig
.authorizeHttpRequests(auth->
auth.requestMatchers("/api/hello")
.permitAll()
.requestMatchers("/api/admin").hasRole("ADMIN")
.anyRequest().authenticated()
).formLogin(Customizer.withDefaults())
.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
//set the user details object
authenticationProvider.setUserDetailsService(databaseUserDetailsService);
//set the password encoder
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
}
Next, run the application and access the APIs. When the login page appears, use the user details from the application_user
table.
You will be successfully authenticated and able to view the response.
The /api/admin
is only accessible to the user sam.