Spring Boot Login and Registration REST API with Spring Security, Hibernate, and MySQL Database

In this tutorial, you will learn how to build login and registration REST APIs for a simple Blog application using Spring Boot, Spring Security, Hibernate, and a MySQL database. We will go through step-by-step instructions to set up the project, configure the necessary dependencies, and implement the login and registration functionality. By the end of this tutorial, you will have a fully functional REST API that allows users to register and login to the application.

Tools and Technologies Used

  • Spring Boot 3
  • JDK 17 or later
  • Spring MVC
  • Spring Security 6
  • Hibernate
  • Maven
  • Spring Data JPA
  • IDE (Eclipse, Spring Tool Suite, IntelliJ IDEA, etc.)
  • MySQL

Step 1: Create a Spring Boot Application

Using Spring Initializr

  1. Navigate to Spring Initializr: Open Spring Initializr in your browser.
  2. Configure the Project:
    • Project: Maven Project
    • Language: Java
    • Spring Boot: 3.2
    • Group: com.rameshfadatare
    • Artifact: springboot-blog-rest-api
    • Name: springboot-blog-rest-api
    • Description: Demo project for Spring Boot Blog Application
    • Package name: com.rameshfadatare
    • Packaging: Jar
    • Java: 17
  3. Add Dependencies:
    • Spring Web
    • Lombok
    • Spring Data JPA
    • Spring Security
    • Dev Tools
    • MySQL Driver
  4. Generate the Project: Click on the Generate button to download the project as a zip file.
  5. Extract the Zip File: Extract the zip file to your preferred location.
  6. Import the Project: Import the extracted project as a Maven project in your preferred IDE.

Step 2: Add Maven Dependencies

Ensure your pom.xml file contains the necessary dependencies. It should look something like this:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</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>com.mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</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-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Explanation:

  • spring-boot-starter-data-jpa: Starter for using Spring Data JPA with Hibernate.
  • spring-boot-starter-web: Starter for building web applications, including RESTful applications using Spring MVC.
  • spring-boot-devtools: Provides fast application restarts, LiveReload, and configurations for enhanced development experience.
  • mysql-connector-java: MySQL JDBC driver to connect with MySQL database.
  • lombok: Java library that helps to reduce boilerplate code.
  • spring-boot-starter-validation: Starter for using Java Bean Validation with Hibernate Validator.
  • spring-boot-starter-security: Starter for using Spring Security.
  • spring-boot-starter-test: Starter for testing Spring Boot applications.

Step 3: Configure MySQL Database

Create a Database

First, create a database in MySQL using the following command:

create database myblog;

Configure Database Connection

Open src/main/resources/application.properties and add the following properties to configure the database connection:

spring.datasource.url = jdbc:mysql://localhost:3306/myblog?useSSL=false&serverTimezone=UTC
spring.datasource.username = root
spring.datasource.password = root

# Hibernate properties
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

logging.level.org.springframework.security=DEBUG

Explanation:

  • spring.datasource.url: JDBC URL of the MySQL database.
  • spring.datasource.username: Username to connect to the database.
  • spring.datasource.password: Password to connect to the database.
  • spring.jpa.properties.hibernate.dialect: Dialect to be used by Hibernate for the MySQL database.
  • spring.jpa.hibernate.ddl-auto: Hibernate property to automatically create/update database schema.
  • logging.level.org.springframework.security: Logging level for Spring Security.

Make sure to replace spring.datasource.username and spring.datasource.password with your MySQL credentials.

Step 4: Model Layer – Create JPA Entities

We will create User and Role JPA entities and establish a many-to-many relationship between them.

User JPA Entity

package com.rameshfadatare.springboot.entity;

import lombok.Data;
import jakarta.persistence.*;
import java.util.Set;

@Data
@Entity
@Table(name = "users", uniqueConstraints = {
        @UniqueConstraint(columnNames = {"username"}),
        @UniqueConstraint(columnNames = {"email"})
})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;
    private String username;
    private String email;
    private String password;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    private Set<Role> roles;
}

Explanation:

  • @Entity: Specifies that the class is an entity and is mapped to a database table.
  • @Table: Specifies the primary table for the annotated entity.
  • @UniqueConstraint: Specifies that the columns in the table must be unique.
  • @Id: Specifies the primary key of an entity.
  • @GeneratedValue: Provides the specification of generation strategies for the values of primary keys.
  • @ManyToMany: Defines a many-to-many relationship between User and Role entities.
  • @JoinTable: Specifies the join table used in the many-to-many relationship.

Role JPA Entity

package com.rameshfadatare.springboot.entity;

import lombok.Getter;
import lombok.Setter;
import jakarta.persistence.*;

@Setter
@Getter
@Entity
@Table(name = "roles")
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(length = 60)
    private String name;
}

Explanation:

  • @Column: Specifies the mapped column for a persistent property or field.
  • @Setter, @Getter: Lombok annotations to generate getter and setter methods.

Step 5: Repository Layer

UserRepository

package com.rameshfadatare.springboot.repository;

import com.rameshfadatare.springboot.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    Optional<User> findByUsernameOrEmail(String username, String email);
    Optional<User> findByUsername(String username);
    Boolean existsByUsername(String username);
    Boolean existsByEmail(String email);
}

Explanation:

  • JpaRepository: Provides JPA related methods such as saving, deleting, and finding entities.
  • Optional: A container object which may or may not contain a non-null value.

RoleRepository

package com.rameshfadatare.springboot.repository;

import com.rameshfadatare.springboot.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface RoleRepository extends JpaRepository<Role, Long> {
    Optional<Role> findByName(String name);
}

Explanation:

  • findByName: Custom method to find a role by its name.

Step 6: Service Layer – CustomUserDetailsService

The CustomUserDetailsService class implements the UserDetailsService interface and provides the implementation for the loadUserByUsername() method.

package com.rameshfadatare.springboot.service;

import com.rameshfadatare.springboot.entity.User;
import com.rameshfadatare.springboot.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import

 org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Set;
import java.util.stream.Collectors;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
          User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
                 .orElseThrow(() -> new UsernameNotFoundException("User not found with username or email: " + usernameOrEmail));

        Set<GrantedAuthority> authorities = user
                .getRoles()
                .stream()
                .map((role) -> new SimpleGrantedAuthority(role.getName())).collect(Collectors.toSet());

        return new org.springframework.security.core.userdetails.User(user.getEmail(),
                user.getPassword(),
                authorities);
    }
}

Explanation:

  • UserDetailsService: Interface which loads user-specific data.
  • loadUserByUsername: Locates the user based on the username or email.
  • GrantedAuthority: Represents an authority granted to an Authentication object.
  • SimpleGrantedAuthority: A concrete implementation of the GrantedAuthority interface.

Step 7: Spring Security Configuration

SecurityConfig

package com.rameshfadatare.springboot.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

    private UserDetailsService userDetailsService;

    public SecurityConfig(UserDetailsService userDetailsService){
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public static PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(
                                 AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .authorizeHttpRequests((authorize) ->
                    authorize.requestMatchers(HttpMethod.GET, "/api/**").permitAll()
                    .requestMatchers("/api/auth/**").permitAll()
                    .anyRequest().authenticated()
                );

        return http.build();
    }
}

Explanation:

  • PasswordEncoder: The BCryptPasswordEncoder is used to encode passwords.
  • AuthenticationManager: This bean is required to authenticate users.
  • SecurityFilterChain: Configures Spring Security to disable CSRF protection, allow public access to certain endpoints, and require authentication for other endpoints.

Step 8: DTO or Payload Classes

LoginDto

package com.rameshfadatare.springboot.payload;

import lombok.Data;

@Data
public class LoginDto {
    private String usernameOrEmail;
    private String password;
}

SignUpDto

package com.rameshfadatare.springboot.payload;

import lombok.Data;

@Data
public class SignUpDto {
    private String name;
    private String username;
    private String email;
    private String password;
}

Explanation:

  • LoginDto: DTO class for login payload.
  • SignUpDto: DTO class for registration payload.
  • @Data: Lombok annotation to generate getter, setter, toString, equals, and hashcode methods.

Step 9: Controller Layer – Login/Sign-in and Register/SignUp REST APIs

AuthController

package com.rameshfadatare.springboot.controller;

import com.rameshfadatare.springboot.entity.Role;
import com.rameshfadatare.springboot.entity.User;
import com.rameshfadatare.springboot.payload.LoginDto;
import com.rameshfadatare.springboot.payload.SignUpDto;
import com.rameshfadatare.springboot.repository.RoleRepository;
import com.rameshfadatare.springboot.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostMapping("/signin")
    public ResponseEntity<String> authenticateUser(@RequestBody LoginDto loginDto) {
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
            loginDto.getUsernameOrEmail(), loginDto.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        return new ResponseEntity<>("User signed-in successfully!", HttpStatus.OK);
    }

    @PostMapping("/signup")
    public ResponseEntity<?> registerUser(@RequestBody SignUpDto signUpDto) {
        if (userRepository.existsByUsername(signUpDto.getUsername())) {
            return new ResponseEntity<>("Username is already taken!", HttpStatus.BAD_REQUEST);
        }

        if (userRepository.existsByEmail(signUpDto.getEmail())) {
            return new ResponseEntity<>("Email is already taken!", HttpStatus.BAD_REQUEST);
        }

        User user = new User();
        user.setName(signUpDto.getName());
        user.setUsername(signUpDto.getUsername());
        user.setEmail(signUpDto.getEmail());
        user.setPassword(passwordEncoder.encode(signUpDto.getPassword()));

        Role roles = roleRepository.findByName("ROLE_ADMIN").get();
        user.setRoles(Collections.singleton(roles));

        userRepository.save(user);

        return new ResponseEntity<>("User registered successfully", HttpStatus.OK);
    }
}

Explanation:

  • authenticateUser: Handles user login. Authenticates the user and sets the authentication context.
  • registerUser: Handles user registration. Checks if the username or email already exists, encodes the password, assigns the role, and saves the user.

Step 10: Run Spring Boot Application

You can run the application in two ways:

  1. Using Maven: From the root directory of the application, run the following command:
    $ mvn spring-boot:run
    
  2. Using IDE: Run the Application.main() method as a standalone Java application.

Step 11: Test using Postman

Refer to the following screenshots to test the Login and Registration REST APIs using Postman.

SignUp REST API

SignUp REST API

SignIn/Login REST API

SignIn REST API

GitHub Repository

You can find the complete source code for this tutorial in the following GitHub repository: GitHub Repository

Udemy Course

For more in-depth knowledge and practical implementations, check out this Udemy course: Building Real-Time REST APIs with Spring Boot and Deploy on AWS Cloud

Leave a Comment

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

Scroll to Top