Saturday, February 11, 2023

Spring Boot: Secure your application with JDBC-Based Authentication

spring framework,spring boot,java,hibernate,spring security,programming,software development,technology
The previous Spring Security tutorial taught us to configure JDBC authentication using the Spring Security recommended database table. The Spring Security Framework is so flexible that we can use our custom database table for JDBC authentication. So in this tutorial, we connect our custom database table with Spring Security for JDBC authentication.

👉 First, we will create a registration service through which we can create a new student. Then configure the student database table for JDBC authentication.

POM.XML

The Spring Boot project's pom.xml is shown below:
<?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.7</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.raven</groupId>
	<artifactId>spring-boot-security-authorization-custom-table</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-security-authorization-custom-table</name>
	<description>Spring Boot project to manage user in custom table in Spring Security</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jdbc</artifactId>
		</dependency>
		<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.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.29</version>
		</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>
		<finalName>spring-boot-security-with-custom-table</finalName>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
Spring Boot version 2.7.6 is what we use. This version of Spring Boot, Spring Framework, and Spring Security is 5.3.24 and 5.7.5, respectively. To implement Spring Security in this application, we have added the spring-boot-starter-security dependency.

Entity

Create a model package under the root package. Create the Student entity within this model package:
package com.raven.springbootsecurityauthorizationcustomtable.model;
import javax.persistence.*;

@Entity
@Table(name = "STUDENT")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name = "full_name", length = 50)
    private String fullName;

    @Column(name = "phone", length = 15)
    private String phone;

    @Column(name = "email", length = 60)
    private String email;

    @Column(name = "pwd", length = 200)
    private String pwd;

    @Column(name = "role", length = 40)
    private String role;

    public Student() {
    }

    public Student(String fullName, String phone, String email, String pwd, String role) {
        this.fullName = fullName;
        this.phone = phone;
        this.email = email;
        this.pwd = pwd;
        this.role = role;
    }

    public long getId() { return id;}

    public String getFullName() { return fullName; }

    public void setFullName(String fullName) { this.fullName = fullName; }

    public String getPhone() { return phone; }

    public void setPhone(String phone) { this.phone = phone; }

    public String getEmail() { return email;}

    public void setEmail(String email) { this.email = email; }

    public String getPwd() { return pwd; }

    public void setPwd(String pwd) { this.pwd = pwd;}

    public String getRole() { return role; }

    public void setRole(String role) { this.role = role; }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", fullName='" + fullName + '\'' +
                ", phone='" + phone + '\'' +
                ", email='" + email + '\'' +
                ", pwd='" + pwd + '\'' +
                ", role='" + role + '\'' +
                '}';
    }
}
So by leveraging the Spring Data JPA Framework, we are creating a database table of the name STUDENT. We use this database table later to configure JDBC authentication. Before that, we will develop a service to save new student details with encrypted credentials.

Repository

Using the Spring Data JPA Framework, we can also save, update, or retrieve records from the database table. For this, we have to create a JPA repository interface. To do so, first, create a package named repository. Then create an interface with the name IStudentRepository inside this package:
package com.raven.springbootsecurityauthorizationcustomtable.repository;

import com.raven.springbootsecurityauthorizationcustomtable.model.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface IStudentRepository extends JpaRepository<Student, Long> {
    Student findByEmail(String email);
}
As you can see, this interface extends another interface called JpaRepository; this allows us to automatically generate code for all CRUD operations at runtime. We passed the Student entity and the data type of the primary key to the JpaRepository. So we tell the Spring Data JPA Framework that all CRUD operations should be performed on the STUDENT table.

Service

We will now create the service layer in order to implement the business logic. To begin, create a package called service. Then, within this package, create a class called StudentService:
package com.raven.springbootsecurityauthorizationcustomtable.service;
// imports are omitted

@Service
public class StudentService implements UserDetailsService {
    private final IStudentRepository studentRepository;
    private final PasswordEncoder passwordEncoder;

    @Autowired
    public StudentService(IStudentRepository studentRepository, PasswordEncoder passwordEncoder) {
        this.studentRepository = studentRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public ResponseEntity<String> registerStudent(Student _student) {
        ResponseEntity<String> responseEntity;

        try {
            // PASSWORD ENCRYPTION
            String _pwd = this.passwordEncoder.encode(_student.getPwd());
            _student.setPwd(_pwd);

            Student studentSaved = this.studentRepository.save(_student);
            responseEntity = ResponseEntity
                    .status(HttpStatus.CREATED)
                    .body("Student details are successfully saved.");
        } catch (Exception exception) {
            responseEntity = ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("Error while saving : " + exception.getMessage());
        }

        return responseEntity;
    }
}
We have annotated this class with @Service in order for Spring Framework to recognize it as a bean.

We injected two beans, as you can see, using the Constructor injection: IStudentRepository and PasswordEncoder. IStudentRepository is used to perform CRUD operations on the student entity. 

The PasswordEncoder interface and its various implementation classes, such as NoOpPasswordEncoder, BCryptPasswordEncoder, SCryptPasswordEncoder, and so on, are provided by the Spring Security Framework. Using the PasswordEncoder, we can convert our plain text password into an encoded value (or a hash string). If we use BCryptPasswordEncoder as our PasswordEncoder, for instance, the BCrypt hashing algorithm will be used to hash the plain text string. For this tutorial, we have configured BCryptPasswordEncoder as the PasswordEncoder in the SecurityConfiguration class.

Inside registerStudent(), we first encode (hashed) the plain text password by calling the PasswordEncoder bean's encode() method. Then we assign this encoded password to the student object and save the student entity to the database.

Controller

Create a controller package under the root package. Create two controller classes within this controller package: StudentController and WelcomeController.

Here is a snippet of code from the StudentController class:
package com.raven.springbootsecurityauthorizationcustomtable.controller;
// imports are omitted

@RestController
public class StudentController {
    private final StudentService studentService;

    @Autowired
    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @PostMapping("/registerStudent")
    public ResponseEntity<String> registerStudent(@RequestBody Student _student) {
        ResponseEntity<String> responseEntity = this.studentService
                .registerStudent(_student);

        return responseEntity;
    }

    @GetMapping("/myCourses")
    public String myCourses() {
        return "Enrolled courses:" +
                "<ul>" +
                "<li>Full Stack JAVA Developer (85% done)</li>" +
                "<li>Microservices with Spring Boot (55 % done)</li>" +
                "<li>Docker guide : Beginner to Master (65% done)</li>" +
                "</ul>";
    }
}
This class is annotated with @RestController, which tells the Spring Container that it will be used for a REST-based service. In addition, we've made two HTTP endpoints available as REST services: /registerStudent and /myCourses.

The following is a code snippet from the WelcomeController class:
package com.raven.springbootsecurityauthorizationcustomtable.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WelcomeController {
    @GetMapping("/welcome")
    public String welcome() {
        return "Available courses:" +
                "<ul>" +
                "<li>Learn JAVA : Beginner to Master</li>" +
                "<li>Full Stack JAVA Developer</li>" +
                "<li>Microservices with Spring Boot</li>" +
                "<li>Complete Web Development</li>" +
                "<li>Wordpress for Beginner</li>" +
                "<li>Complete Python Development</li>" +
                "<li>Docker guide : Beginner to Master</li>" +
                "<li>Node.js : Ultimate guide</li>" +
                "</ul>";
    }
}
This class is also marked with the annotation @RestController. This class has also made another HTTP endpoint - /welcome - available.

Security Configuration

Create a new package under the root package called configuration. Create a class called SecurityConfiguration within the configuration package and update it with the following code:
package com.raven.springbootsecurityauthorizationcustomtable.configuration;
// imports are omitted

@Configuration
public class SecurityConfiguration {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        /* CUSTOM SECURITY */
        httpSecurity.csrf().disable()
                .authorizeHttpRequests()
                .antMatchers("/myCourses").authenticated()
                .antMatchers("/welcome", "/registerStudent").permitAll()
                .and().formLogin()
                .and().httpBasic();
        return httpSecurity.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
So, in the first antMatchers() of securityFilterChain(), we mentioned the endpoint /myCourses and invoked the authenticated() method; this means the /myCourses endpoint will be secured by Spring Security. In the following antMatchers, we added two endpoints: /welcome and /registerStudent, and invoked a method called permitAll(), so Spring Security will no longer authenticate these REST services, and anyone can access them.

In addition, we defined a bean of type PasswordEncoder. BCryptPasswordEncoder is selected as our password encoder here.

Run the Application

Run the application and register a new student in Postman by calling /registerStudent.
Student information is saved in the database with a hashed password. If you look at the student table in the database, you will notice that the value of the pwd column is not plain text.

Now we must configure Spring Security so that students can access secure resources by logging in with their password and email address.

Configure UserDetailsService

To perform authentication by fetching user details from the custom database table, we have to use the UserDetailsService interface, which is present in the Spring Security Framework. This interface includes the loadUserByUsername(String username) method. This method accepts the user name, loads the user from the storage system, and returns its implementation class, which is UserDetails.

So, in our code, we override the loadUserByUsername(String username) method and write logic to retrieve the user from a custom database table, construct the UserDetails object, and send it back to the Spring Security Framework so that it can perform the authentication.

So we have modified the StudentService class as below:
package com.raven.springbootsecurityauthorizationcustomtable.service;
// imports are omitted

@Service
public class StudentService implements UserDetailsService {
    private final IStudentRepository studentRepository;
    private final PasswordEncoder passwordEncoder;

    @Autowired
    public StudentService(IStudentRepository studentRepository, PasswordEncoder passwordEncoder) {
        this.studentRepository = studentRepository;
        this.passwordEncoder = passwordEncoder;
    }
    
    // service for register student is omitted
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String _username, _password;
        List<GrantedAuthority> _authorities;

        Student student = this.studentRepository.findByEmail(username);
        if (student != null) {
            _username = student.getEmail();
            _password = student.getPwd();
            _authorities = new ArrayList<>();
            _authorities.add(new SimpleGrantedAuthority(student.getRole()));
        } else {
            throw new UsernameNotFoundException("Student not found with : " + username);
        }

        return new User(_username, _password, _authorities);
    }
}
As you can see we have implemented the UserDetailsService and overrided the loadUserByUsername(String username) method. As we are considering email as the user name that is why we are fetching the Student object by calling findByEmail() method of the student repository. Then we create the User object by passing a username, password, and authorities to its constructor. We create this User object because it is the implementation class of the UserDetails interface. Now, this User object will be sent back to the Spring Security Framework, and the actual authentication will happen inside the Spring Security Framework.

Now restart the application and try to access the /myCourses endpoint. You will be redirected to the login page. You can use previously created student credentials to log in.

So whenever we are trying to perform authentication, the request is handed over to the default authentication provider, which is the DaoAuthentication provider of the Spring Security Framework. Now the dao authentication provider will look for the implementation class of UserDetailsService and it will find StudentService as the implementation class. We cannot use multiple implementation classes of UserDetailsService in an application. As we have implemented the UserDetailsService in the StudentService class, we cannot use the InMemoryUserDetailsManager or the JDBCUserDetailsManager as authentication providers. 

You can download the source code from here.
Happy coding!!! 😊
in

No comments:

Post a Comment

Popular posts