How to Make a Spring Boot App with JWT Authentication and PostgreSQL

Spring Boot is a popular framework for creating web applications in Java. It provides many features and conventions that make development easier and faster.

In this blog post, I will show you how to create a simple Spring Boot app that uses JWT for authentication and PostgreSQL for a persistence layer. I will assume that you have some basic knowledge of Java, Spring Boot, Maven, and SQL. I will also be using IntelliJ IDEA as my IDE, but you can use any other IDE of your choice.

Step 1: Create a Spring Boot Project

The first step is to create a new Spring Boot project using the Spring Initializer website (https://start.spring.io/). Choose the following options:

  • Project: Maven
  • Language: Java
  • Spring Boot: 3.1.2 as of writing this
  • Project Metadata:
    • Group: com.app
    • Artifact: springboot-jwt-postgres
    • Name: springboot-jwt-postgres
    • Description: Demo project for Spring Boot with JWT and PostgreSQL
    • Package name: com.app.springboot-jwt-postgres
    • Packaging: Jar
    • Java: 17
  • Dependencies:
    • Spring Web
    • Spring Security
    • Spring Data JPA
    • PostgreSQL Driver
    • Lombok
    • Validation

The resulting webpage should look something like this:

After selecting these options, click on Generate to download the project as a zip file. Then, extract the zip file and open it in your IDE.

Step 2: Configure the Application Properties

The next step is to configure the application properties file (src/main/resources/application.properties) to set up the database connection and some security properties. You can use the following code:

# Database configuration
spring.datasource.url=jdbc:postgresql://localhost:5432/springboot_jwt_postgres
spring.datasource.username=postgres
spring.datasource.password=postgres

# JPA configuration
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update

# JWT configuration
# This is the secret key used to sign the JWTs. You should use a more secure and random value in production.
jwt.secret=823012395647503095848585629229488318125967956976077303153367268008550106317041442963571737703173703074958557361211053894437266913428830158109326184433594384517646730543359896256927103502344702260527967762
jwt.expiration=86400000

Make sure that you have PostgreSQL installed and running on your machine, and that you have created a database named springboot_jwt_postgres with the username and password postgres. You can also change these values according to your preferences.

Step 3: Create the Entity Classes

The next step is to create the entity classes that represent the data model of our application. We will use JPA annotations to map these classes to the database tables. We will also use Lombok annotations to generate getters, setters, constructors, and toString methods automatically.

We will create two entity classes: User and Role. A user can have one or more roles, and a role can be assigned to one or more users. Therefore, we will use a many-to-many relationship between these classes.

Create a new package named com.example.springbootjwtpostgres.entity and create the following classes:

User.java

package com.app.springbootjwtpostgres.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.HashSet;
import java.util.Set;

@Entity // This annotation marks this class as an entity class that will be mapped to a database table.
@Table(name = "users") // This annotation specifies the name of the table that this entity class will be mapped to.
@Data // This annotation generates getters, setters, constructors, and toString methods for this class.
@NoArgsConstructor // This annotation generates a no-argument constructor for this class.
@AllArgsConstructor // This annotation generates an all-argument constructor for this class.
public class User {

    @Id // This annotation marks this field as the primary key of the table.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // This annotation specifies that this field will be generated automatically by the database.
    private Long id;

    @Column(name = "username", nullable = false, unique = true) // This annotation specifies the name, nullability, and uniqueness of the column that this field will be mapped to.
    private String username;

    @Column(name = "password", nullable = false) // This annotation specifies the name and nullability of the column that this field will be mapped to.
    private String password;

    @Column(name = "email", nullable = false, unique = true) // This annotation specifies the name, nullability, and uniqueness of the column that this field will be mapped to.
    private String email;

    @ManyToMany(fetch = FetchType.EAGER) // This annotation specifies that this field represents a many-to-many relationship with another entity class, and that the related entities will be fetched eagerly (loaded together with this entity).
    @JoinTable(name = "user_roles", // This annotation specifies the name of the join table that will store the relationship between this entity class and another entity class.
            joinColumns = @JoinColumn(name = "user_id"), // This annotation specifies the name of the column in the join table that references the primary key of this entity class.
            inverseJoinColumns = @JoinColumn(name = "role_id")) // This annotation specifies the name of the column in the join table that references the primary key of another entity class.
    private Set<Role> roles = new HashSet<>(); // This field stores a set of roles that belong to this user.

}

Role.java

package com.app.springbootjwtpostgres.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity // This annotation marks this class as an entity class that will be mapped to a database table.
@Table(name = "roles") // This annotation specifies the name of the table that this entity class will be mapped to.
@Data // This annotation generates getters, setters, constructors, and toString methods for this class.
@NoArgsConstructor // This annotation generates a no-argument constructor for this class.
@AllArgsConstructor // This annotation generates an all-argument constructor for this class.
public class Role {

    @Id // This annotation marks this field as the primary key of the table.
    @GeneratedValue(strategy = GenerationType.IDENTITY) // This annotation specifies that this field will be generated automatically by the database.
    private Long id;

    @Column(name = "name", nullable = false, unique = true) // This annotation specifies the name, nullability, and uniqueness of the column that this field will be mapped to.
    private String name;

    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY) // This annotation specifies that this field represents a many-to-many relationship with another entity class, and that the related entities will be fetched lazily (loaded on demand).
    private Set<User> users = new HashSet<>(); // This field stores a set of users that have this role.

}

Step 4: Create the Repository Interfaces

The next step is to create the repository interfaces that provide methods for accessing and manipulating the data in the database. We will use Spring Data JPA to create these interfaces, which will automatically generate the implementation classes at runtime.

Create a new package named com.example.springbootjwtpostgres.repository and create the following interfaces:

UserRepository.java

package com.app.springbootjwtpostgres.repository;


import com.app.springbootjwtpostgres.entity.User;
import org.springframework.data.jpa.repository.JpaRepository; // This interface provides basic CRUD methods for working with JPA entities.
import org.springframework.stereotype.Repository; // This annotation marks this interface as a repository component that will be managed by Spring.

import java.util.Optional;

@Repository // This annotation marks this interface as a repository component that will be managed by Spring.
public interface UserRepository extends JpaRepository<User, Long> { // This interface extends JpaRepository interface and specifies the entity type and the primary key type.

    Optional<User> findUserByUsername(String username); // This method declares a custom query method that will find a user by username. Spring Data JPA will automatically generate the implementation based on the method name.

    Boolean existsByUsername(String username); // This method declares a custom query method that will check if a user exists by username. Spring Data JPA will automatically generate the implementation based on the method name.

    Boolean existsByEmail(String email); // This method declares a custom query method that will check if a user exists by email. Spring Data JPA will automatically generate the implementation based on the method name.

}

RoleRepository.java

package com.app.springbootjwtpostgres.repository;


import com.app.springbootjwtpostgres.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository; // This interface provides basic CRUD methods for working with JPA entities.
import org.springframework.stereotype.Repository; // This annotation marks this interface as a repository component that will be managed by Spring.

import java.util.Optional;

@Repository // This annotation marks this interface as a repository component that will be managed by Spring.
public interface RoleRepository extends JpaRepository<Role, Long> { // This interface extends JpaRepository interface and specifies the entity type and the primary key type.

    Optional<Role> findRoleByName(String name); // This method declares a custom query method that will find a role by name.

}

Step 5: Create the Service Classes

The next step is to create the service classes that provide the business logic for our application. We will use the @Service annotation to mark these classes as service components that will be managed by Spring. We will also use the @Autowired annotation to inject the repository interfaces into these classes.

Create a new package named com.example.springbootjwtpostgres.service and create the following classes:

UserService.java

package com.app.springbootjwtpostgres.service;

import com.app.springbootjwtpostgres.entity.Role;
import com.app.springbootjwtpostgres.entity.User;
import com.app.springbootjwtpostgres.repository.RoleRepository;
import com.app.springbootjwtpostgres.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service // This annotation marks this class as a service component that will be managed by Spring.
public class UserService {

    @Autowired // This annotation allows us to inject the UserRepository interface into this class.
    private UserRepository userRepository;

    @Autowired // This annotation allows us to inject the RoleRepository interface into this class.
    private RoleRepository roleRepository;

    public User saveUser(User user) { // This method saves a user entity to the database.
        user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword())); // This line encodes the user's password using the PasswordEncoder interface.
        return userRepository.save(user); // This line saves the user entity using the UserRepository interface.
    }

    public User findByUsername(String username) { // This method finds a user entity by username.
        return userRepository.findUserByUsername(username).orElse(null); // This line returns the user entity if found, or null otherwise, using the UserRepository interface.
    }

    public List<User> findAllUsers() { // This method returns a list of all user entities in the database.
        return userRepository.findAll(); // This line returns the list of user entities using the UserRepository interface.
    }

    public boolean existsByUsername(String username) { // This method checks if a user entity exists by username.
        return userRepository.existsByUsername(username); // This line returns true or false using the UserRepository interface.
    }

    public boolean existsByEmail(String email) { // This method checks if a user entity exists by email.
        return userRepository.existsByEmail(email); // This line returns true or false using the UserRepository interface.
    }

    public User addRoleToUser(String username, String roleName) { // This method adds a role entity to a user entity by their names.
        User user = userRepository.findUserByUsername(username).orElse(null); // This line finds the user entity by username using the UserRepository interface.
        Role role = roleRepository.findRoleByName(roleName).orElse(null); // This line finds the role entity by name using the RoleRepository interface.

        if (user != null && role != null) { // This block checks if both entities are not null.
            user.getRoles().add(role); // This line adds the role entity to the set of roles of the user entity.
            return userRepository.save(user); // This line saves the updated user entity using the UserRepository interface and returns it.
        }

        return null; // This line returns null if either entity is null.
    }

}

RoleService.java

package com.app.springbootjwtpostgres.service;

import com.app.springbootjwtpostgres.entity.Role;
import com.app.springbootjwtpostgres.repository.RoleRepository;
import org.springframework.beans.factory.annotation.Autowired; // This annotation allows us to inject other components into this class.
import org.springframework.stereotype.Service; // This annotation marks this class as a service component that will be managed by Spring.

import java.util.List;

@Service // This annotation marks this class as a service component that will be managed by Spring.
public class RoleService {

    @Autowired // This annotation allows us to inject the RoleRepository interface into this class.
    private RoleRepository roleRepository;

    public Role saveRole(Role role) { // This method saves a role entity to the database.
        return roleRepository.save(role); // This line saves the role entity using the RoleRepository interface and returns it.
    }

    public Role findByName(String name) { // This method finds a role entity by name.
        return roleRepository.findRoleByName(name).orElse(null); // This line returns the role entity if found, or null otherwise, using the RoleRepository interface.
    }

    public List<Role> findAllRoles() { // This method returns a list of all role entities in the database.
        return roleRepository.findAll(); // This line returns the list of role entities using the RoleRepository interface.
    }

}

Step 6: Create the DTO Classes

The next step is to create the DTO (Data Transfer Object) classes that represent the data that will be exchanged between the client and the server. We will use these classes to transfer only the necessary data and avoid exposing sensitive or irrelevant data. We will also use Lombok annotations to generate getters, setters, constructors, and toString methods automatically.

We will create three DTO classes: SignupRequest, LoginRequest, and JwtResponse. The SignupRequest class will contain the data for registering a new user, such as username, password, email, and roles. The LoginRequest class will contain the data for logging in an existing user, such as username and password. The JwtResponse class will contain the data for sending back a JWT token and some user information after a successful login.

Create a new package named com.example.springbootjwtpostgres.dto and create the following classes:

SignupRequest.java

package com.app.springbootjwtpostgres.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Set;

@Data // This annotation generates getters, setters, constructors, and toString methods for this class.
@NoArgsConstructor // This annotation generates a no-argument constructor for this class.
@AllArgsConstructor // This annotation generates an all-argument constructor for this class.
public class SignupRequest {

    private String username; // This field stores the username of the user.

    private String password; // This field stores the password of the user.

    private String email; // This field stores the email of the user.

    private Set<String> roles; // This field stores a set of role names that belong to the user.

}

LoginRequest.java

package com.app.springbootjwtpostgres.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data // This annotation generates getters, setters, constructors, and toString methods for this class.
@NoArgsConstructor // This annotation generates a no-argument constructor for this class.
@AllArgsConstructor // This annotation generates an all-argument constructor for this class.
public class LoginRequest {

    private String username; // This field stores the username of the user.

    private String password; // This field stores the password of the user.

}

JwtResponse.java

package com.app.springbootjwtpostgres.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data // This annotation generates getters, setters, constructors, and toString methods for this class.
@NoArgsConstructor // This annotation generates a no-argument constructor for this class.
@AllArgsConstructor
public class JwtResponse {

    private String token; // This field stores the JWT token.

    private String type = "Bearer"; // This field stores the token type.

    private Long id; // This field stores the id of the user.

    private String username; // This field stores the username of the user.

    private String email; // This field stores the email of the user.

    private List<String> roles; // This field stores a list of role names that belong to the user.

    public JwtResponse(String jwt, Long id, String username, String email, List<String> roles) {
        this.token = jwt;
        this.id = id;
        this.username = username;
        this.email = email;
        this.roles = roles;
    }
}

Step 7: Create the Controller Classes

The next step is to create the controller classes that handle the HTTP requests and responses for our application. We will use the @RestController annotation to mark these classes as controller components that will be managed by Spring. We will also use the @RequestMapping annotation to specify the base URL path for these classes. We will also use other annotations such as @PostMapping, @GetMapping, @RequestBody, @PathVariable, @PreAuthorize, etc. to define the HTTP methods, parameters, and security rules for each endpoint.

We will create two controller classes: AuthController and UserController. The AuthController class will handle the requests related to authentication, such as signup and login. The UserController class will handle the requests related to users, such as getting all users or adding a role to a user.

Create a new package named com.example.springbootjwtpostgres.controller and create the following classes:

AuthController.java

package com.app.springbootjwtpostgres.controller;

import com.app.springbootjwtpostgres.dto.JwtResponse;
import com.app.springbootjwtpostgres.dto.LoginRequest;
import com.app.springbootjwtpostgres.dto.SignupRequest;
import com.app.springbootjwtpostgres.entity.Role;
import com.app.springbootjwtpostgres.entity.User;
import com.app.springbootjwtpostgres.security.JwtUtils;
import com.app.springbootjwtpostgres.service.RoleService;
import com.app.springbootjwtpostgres.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@RestController // This annotation marks this class as a controller component that will be managed by Spring and that will return JSON responses by default.
@RequestMapping("/api/auth") // This annotation specifies the base URL path for this controller class.
@CrossOrigin(origins = "*", maxAge = 3600) // This annotation allows cross-origin requests from any origin and with a maximum age of 3600 seconds (1 hour).
public class AuthController {

    @Autowired // This annotation allows us to inject the AuthenticationManager interface into this class.
    private AuthenticationManager authenticationManager;

    @Autowired // This annotation allows us to inject the UserService class into this class.
    private UserService userService;

    @Autowired // This annotation allows us to inject the RoleService class into this class.
    private RoleService roleService;

    @Autowired // This annotation allows us to inject the JwtUtils class into this class.
    private JwtUtils jwtUtils;

    @PostMapping("/signup") // This annotation specifies that this method handles POST requests at the /signup endpoint of this controller class.
    public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signupRequest) { // This method takes a valid SignupRequest object as the request body and returns a ResponseEntity object as the response.

        if (userService.existsByUsername(signupRequest.getUsername())) { // This block checks if the username already exists in the database using the UserService class.
            return ResponseEntity.badRequest().body("Error: Username is already taken!"); // If yes, it returns a bad request response with an error message.
        }

        if (userService.existsByEmail(signupRequest.getEmail())) { // This block checks if the email already exists in the database using the UserService class.
            return ResponseEntity.badRequest().body("Error: Email is already in use!"); // If yes, it returns a bad request response with an error message.
        }
        Set<Role> roles = new HashSet<>(); // This line creates a new set of Role objects.
        User user = new User(
                0L,
                signupRequest.getUsername(),
                signupRequest.getPassword(),
                signupRequest.getEmail(), roles); // This line creates a new User object with the data from the SignupRequest object.

        Set<String> strRoles = signupRequest.getRoles(); // This line gets the set of role names from the SignupRequest object.


        if (strRoles == null || strRoles.isEmpty()) { // This block checks if the set of role names is null or empty. If yes, it assigns a default role of "user" to the user.
            Role userRole = roleService.findByName("user"); // This line finds the role entity with the name "user" using the RoleService class.
            if (userRole == null) { // This block checks if the role entity is null. If yes, it creates a new role entity with the name "user" and saves it to the database using the RoleService class.
                userRole = new Role();
                userRole.setName("user");
                roleService.saveRole(userRole);
            }
            roles.add(userRole); // This line adds the role entity to the set of roles.
        } else { // If no, it iterates over the set of role names and finds or creates the corresponding role entities and adds them to the set of roles.
            strRoles.forEach(role -> {
                switch (role) {
                    case "admin":
                        Role adminRole = roleService.findByName("admin");
                        if (adminRole == null) {
                            adminRole = new Role();
                            adminRole.setName("admin");
                            roleService.saveRole(adminRole);
                        }
                        roles.add(adminRole);
                        break;
                    case "moderator":
                        Role modRole = roleService.findByName("moderator");
                        if (modRole == null) {
                            modRole = new Role();
                            modRole.setName("moderator");
                            roleService.saveRole(modRole);
                        }
                        roles.add(modRole);
                        break;
                    default:
                        Role userRole = roleService.findByName("user");
                        if (userRole == null) {
                            userRole = new Role();
                            userRole.setName("user");
                            roleService.saveRole(userRole);
                        }
                        roles.add(userRole);
                }
            });
        }

        user.setRoles(roles); // This line sets the set of roles to the user object.
        userService.saveUser(user); // This line saves the user object to the database using the UserService class.

        return ResponseEntity.ok("User registered successfully!"); // This line returns an ok response with a success message.
    }

    @PostMapping("/login") // This annotation specifies that this method handles POST requests at the /login endpoint of this controller class.
    public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { // This method takes a valid LoginRequest object as the request body and returns a ResponseEntity object as the response.

        Authentication authentication = authenticationManager.authenticate( // This line authenticates the user using the AuthenticationManager interface and returns an Authentication object that represents the authenticated user.
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); // This line creates a new UsernamePasswordAuthenticationToken object with the username and password from the LoginRequest object.

        SecurityContextHolder.getContext().setAuthentication(authentication); // This line sets the Authentication object to the security context of the current thread using the SecurityContextHolder class.
        String jwt = jwtUtils.generateJwtToken(authentication); // This line generates a JWT token using the JwtUtils class and the Authentication object.

        UserDetails userDetails = (UserDetails) authentication.getPrincipal(); // This line gets the UserDetails object from the Authentication object.
        List<String> roles = userDetails.getAuthorities().stream() // This line gets a list of role names from the UserDetails object by streaming over its authorities and mapping them to their names.
                .map(item -> item.getAuthority())
                .collect(Collectors.toList());
        User currentUser = userService.findByUsername(userDetails.getUsername());
        return ResponseEntity.ok(new JwtResponse(jwt,currentUser.getId(), userDetails.getUsername(), currentUser.getEmail(), roles)); // This line returns an ok response with a new JwtResponse object that contains the JWT token and some user information.
    }

}

UserController.java

package com.app.springbootjwtpostgres.controller;

import com.app.springbootjwtpostgres.entity.User;
import com.app.springbootjwtpostgres.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController // This annotation marks this class as a controller component that will be managed by Spring and that will return JSON responses by default.
@RequestMapping("/api/users") // This annotation specifies the base URL path for this controller class.
@CrossOrigin(origins = "*", maxAge = 3600) // This annotation allows cross-origin requests from any origin and with a maximum age of 3600 seconds (1 hour).
public class UserController {

    @Autowired // This annotation allows us to inject the UserService class into this class.
    private UserService userService;

    @GetMapping("/") // This annotation specifies that this method handles GET requests at the / endpoint of this controller class.
    @PreAuthorize("hasRole('ADMIN')") // This annotation specifies that this method can only be accessed by users who have the role of "ADMIN".
    public ResponseEntity<List<User>> getAllUsers() { // This method returns a ResponseEntity object that contains a list of all user entities in the database as the body.
        List<User> users = userService.findAllUsers(); // This line gets the list of all user entities using the UserService class.
        return ResponseEntity.ok(users); // This line returns an ok response with the list of user entities as the body.
    }

    @PostMapping("/add-role/{username}/{roleName}") // This annotation specifies that this method handles POST requests at the /add-role/{username}/{roleName} endpoint of this controller class, where {username} and {roleName} are path variables that represent the username and role name respectively.
    public ResponseEntity<?> addRoleToUser(@PathVariable String username, @PathVariable String roleName) { // This method takes two path variables as parameters and returns a ResponseEntity object as the response.
        User user = userService.addRoleToUser(username, roleName); // This line adds a role entity to a user entity by their names using the UserService class and returns the updated user entity.
        if (user != null) { // This block checks if the user entity is not null. If yes, it returns an ok response with a success message.
            return ResponseEntity.ok("Role added successfully to user!");
        }

        return ResponseEntity.badRequest().body("Error: User or role not found!"); // If no, it returns a bad request response with an error message.
    }

}

Step 8: Create the Security Classes

The next step is to create the security classes that provide the security configuration and functionality for our application. We will use Spring Security to implement JWT authentication and authorization for our endpoints. We will also use some annotations such as @EnableWebSecurity, @EnableGlobalMethodSecurity, @Bean, and etc. To enable and customize the security features.

We will create four security classes: WebSecurityConfig, UserDetailsServiceImpl, AuthEntryPointJwt, and JwtUtils. The WebSecurityConfig class will extend the WebSecurityConfigurerAdapter class and override some methods to configure the security rules and filters for our endpoints. The UserDetailsServiceImpl class will implement the UserDetailsService interface and override the loadUserByUsername method to load a user’s details from the database by username. The AuthEntryPointJwt class will implement the AuthenticationEntryPointinterface and override the commence method to handle unauthorized requests and send back an error response. The JwtUtils class will provide some utility methods for generating and validating JWT tokens.

Added the following new dependency to your POM file:

		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.11.5</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.11.5</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId>
			<version>0.11.5</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
			<version>6.0.1</version>
		</dependency>

Create a new package named com.example.springbootjwtpostgres.security and create the following classes:

WebSecurityConfig.java

package com.app.springbootjwtpostgres.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration // This annotation marks this class as a configuration component that will be managed by Spring.
@EnableWebSecurity // This annotation enables web security for this application.
@EnableGlobalMethodSecurity(prePostEnabled = true) // This annotation enables global security for method-level annotations, such as @PreAuthorize, with pre- and post-invocation checks enabled.
public class WebSecurityConfig { // This class extends WebSecurityConfigurerAdapter class and overrides some methods to customize web security features.

    @Autowired // This annotation allows us to inject the UserDetailsServiceImpl class into this class.
    private UserDetailsServiceImpl userDetailsService;

    @Autowired // This annotation allows us to inject the AuthEntryPointJwt class into this class.
    private AuthEntryPointJwt unauthorizedHandler;

    @Bean // This annotation marks this method as a bean producer that will be managed by Spring.
    public AuthTokenFilter authenticationJwtTokenFilter() { // This method returns an AuthTokenFilter object as a bean.
        return new AuthTokenFilter(); // This line creates a new AuthTokenFilter object and returns it.
    }

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

    @Bean // This annotation marks this method as a bean producer that will be managed by Spring.
    public PasswordEncoder passwordEncoder() { // This method returns a PasswordEncoder object as a bean.
        return new BCryptPasswordEncoder(); // This line creates a new BCryptPasswordEncoder object and returns it.
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());

        return authProvider;
    }



    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
                .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth ->
                        auth.requestMatchers(new AntPathRequestMatcher("/api/auth/**")).permitAll()
                                .requestMatchers(new AntPathRequestMatcher("/api/test/**")).permitAll()
                                .anyRequest().authenticated()
                );

        http.authenticationProvider(authenticationProvider());

        http.addFilterAfter(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

}

UserDetailsServiceImpl.java

package com.app.springbootjwtpostgres.security;

import com.app.springbootjwtpostgres.entity.Role;
import com.app.springbootjwtpostgres.entity.User;
import com.app.springbootjwtpostgres.service.UserService;
import jakarta.transaction.Transactional;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.stream.Collectors;

@Service // This annotation marks this class as a service component that will be managed by Spring.
public class UserDetailsServiceImpl implements UserDetailsService { // This class implements UserDetailsService interface and overrides its methods.

    @Autowired // This annotation allows us to inject the UserService class into this class.
    private UserService userService;

    @Override // This annotation indicates that this method overrides a method from the interface.
    @Transactional // This annotation marks this method as transactional, meaning that it will be executed within a database transaction.
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // This method loads user details by username and returns a UserDetails object or throws an exception if not found.
        User user = userService.findByUsername(username); // This line finds the user entity by username using the UserService class.

        if (user == null) { // This block checks if the user entity is null. If yes, it throws an exception with an error message.
            throw new UsernameNotFoundException("User Not Found with username: " + username);
        }

        return UserDetailsImpl.build(user); // If no, it returns a new User object (from Spring Security) with the username, password, and authorities of the user entity, using a helper method to map roles to authorities.
    }

    private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Collection<Role> roles) { // This is a helper method that maps
// This is a helper method that maps a collection of role entities to a collection of granted authorities.
        return roles.stream() // This line streams over the collection of role entities.
                .map(role -> new SimpleGrantedAuthority(role.getName())) // This line maps each role entity to a new SimpleGrantedAuthority object with the role name as the authority.
                .collect(Collectors.toList()); // This line collects the mapped authorities into a list and returns it.
    }

}

UserDetailsImpl.java

package com.app.springbootjwtpostgres.security;

import com.app.springbootjwtpostgres.entity.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class UserDetailsImpl implements UserDetails {
    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String email;

    @JsonIgnore
    private String password;

    private Collection<? extends GrantedAuthority> authorities;

    public UserDetailsImpl(Long id, String username, String email, String password,
                           List<GrantedAuthority> authorities) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
    }

    public static UserDetailsImpl build(User user) {
        List<GrantedAuthority> authorities = user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toList());

        return new UserDetailsImpl(
                user.getId(),
                user.getUsername(),
                user.getEmail(),
                user.getPassword(),
                authorities);
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public Long getId() {
        return id;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        UserDetailsImpl user = (UserDetailsImpl) o;
        return Objects.equals(id, user.id);
    }
}

AuthEntryPointJwt.java

package com.app.springbootjwtpostgres.security;

import org.slf4j.Logger; // This class provides methods for logging messages with different levels of severity.
import org.slf4j.LoggerFactory; // This class provides methods for obtaining logger instances.
import org.springframework.security.core.AuthenticationException; // This exception is thrown when an authentication request fails.
import org.springframework.security.web.AuthenticationEntryPoint; // This interface provides methods for handling unauthorized requests.
import org.springframework.stereotype.Component; // This annotation marks this class as a component that will be managed by Spring.

import jakarta.servlet.http.HttpServletRequest; // This class represents an HTTP request.
import jakarta.servlet.http.HttpServletResponse; // This class represents an HTTP response.
import java.io.IOException; // This exception is thrown when an input/output operation fails.

@Component // This annotation marks this class as a component that will be managed by Spring.
public class AuthEntryPointJwt implements AuthenticationEntryPoint { // This class implements AuthenticationEntryPoint interface and overrides its methods.

    private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); // This line creates a logger instance for this class using LoggerFactory class.

    @Override // This annotation indicates that this method overrides a method from the interface.
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // This method handles unauthorized requests and sends back an error response.
        logger.error("Unauthorized error: {}", authException.getMessage()); // This line logs the error message using the logger instance with the error level.
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); // This line sends an error response with the status code 401 (Unauthorized) and the error message using the response object.
    }

}

AuthTokenFilter.java

package com.app.springbootjwtpostgres.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class AuthTokenFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUserNameFromJwtToken(jwt);

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
            logger.error("Cannot set user authentication: {}", e);
        }

        filterChain.doFilter(request, response);
    }

    private String parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");

        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
            return headerAuth.substring(7);
        }

        return null;
    }
}

JwtUtils.java

package com.app.springbootjwtpostgres.security;

import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component // This annotation marks this class as a component that will be managed by Spring.
public class JwtUtils {

    @Value("${jwt.secret}") // This annotation injects the value of jwt.secret property from application properties file into this field.
    private String jwtSecret; // This field stores the secret key used to sign the JWT tokens.

    @Value("${jwt.expiration}") // This annotation injects the value of jwt.expiration property from application properties file into this field.
    private String jwtExpirationMs; // This field stores the expiration time of the JWT tokens in milliseconds.
    private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
    public String generateJwtToken(Authentication authentication) { // This method generates a JWT token based on an Authentication object and returns it as a string.

        UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal(); // This line gets the UserDetailsImpl object from the Authentication object.

        return Jwts.builder() // This line creates a new JWT builder object using Jwts class.
                .setSubject((userPrincipal.getUsername())) // This line sets the subject (username) of the JWT token using the UserDetailsImpl object.
                .setIssuedAt(new Date()) // This line sets the issued at date (current date) of the JWT token using Date class.
                .setExpiration(new Date((new Date()).getTime() + Long.parseLong(jwtExpirationMs))) // This line sets the expiration date (current date plus expiration time) of the JWT token using Date class.
                .signWith(SignatureAlgorithm.HS512, jwtSecret) // This line signs the JWT token with HS512 algorithm and the secret key.
                .compact(); // This line compacts the JWT token into a string and returns it.
    }

    public String getUserNameFromJwtToken(String token) { // This method extracts the username from a JWT token and returns it as a string.
        return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject(); // This line parses the JWT token using the secret key, gets the claims body, and gets the subject (username) and returns it.
    }

    public boolean validateJwtToken(String authToken) { // This method validates a JWT token and returns true or false.
        try { // This block tries to parse the JWT token using the secret key and catch any exception that may occur.
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); // This line parses the JWT token using the secret key.
            return true; // If no exception occurs, it returns true.
        } catch (SignatureException e) { // This block catches a SignatureException, which occurs when the signature of the JWT token is invalid.
            logger.error("Invalid JWT signature: {}", e.getMessage()); // This line prints the error message to the standard error stream.
        } catch (MalformedJwtException e) { // This block catches a MalformedJwtException, which occurs when the format of the JWT token is invalid.
            logger.error("Invalid JWT token: {}", e.getMessage()); // This line prints the error message to the standard error stream.
        } catch (ExpiredJwtException e) { // This block catches an ExpiredJwtException, which occurs when the expiration date of the JWT token is past.
            logger.error("JWT token is expired: {}", e.getMessage()); // This line prints the error message to the standard error stream.
        } catch (UnsupportedJwtException e) { // This block catches an UnsupportedJwtException, which occurs when the type of the JWT token is not supported.
            logger.error("JWT token is unsupported: {}", e.getMessage()); // This line prints the error message to the standard error stream.
        } catch (IllegalArgumentException e) { // This block catches an IllegalArgumentException, which occurs when the argument of the JWT token is invalid or null.
            logger.error("JWT claims string is empty: {}", e.getMessage()); // This line prints the error message to the standard error stream.
        }

        return false; // If any exception occurs, it returns false.
    }

}

Step 9: Test the Application

The final step is to test our application and see if it works as expected. We will use Postman, a tool for testing APIs, to send HTTP requests to our endpoints and check the responses.

Before we start testing, we need to run our application using Maven or our IDE. We also need to insert some role entities into our database using SQL commands or a database client tool.

For example, we can use pgAdmin, a web-based administration tool for PostgreSQL, to connect to our database and run the following SQL commands:

INSERT INTO roles (name) VALUES ('user');
INSERT INTO roles (name) VALUES ('moderator');
INSERT INTO roles (name) VALUES ('admin');

This will create three role entities with names ‘user’, ‘moderator’, and ‘admin’ in our database.

Now, we can open Postman and create a new collection named ‘Spring Boot JWT Postgres’. Then, we can create some requests for testing our endpoints.

Signup Request

We can create a POST request with URL http://localhost:8080/api/auth/signup and body:

{
    "username": "test",
    "password": "test",
    "email": "[email protected]",
    "roles": ["user", "moderator"]
}

This request will register a new user with username ‘test’, password ‘test’, email ‘[email protected]’, and roles ‘user’ and ‘moderator’.

We can send this request and check the response. We should get a status code of 200 OK and a body:

User registered successfully!

This means that our signup endpoint works as expected and our user entity has been saved to the database with the encoded password and assigned roles.

Login Request

We can create another POST request with URL http://localhost:8080/api/auth/login and body:

{
    "username": "test",
    "password": "test"
}

This request will log in an existing user with username ‘test’ and password ‘test’.

We can send this request and check the response. We should get a status code of 200 OK and a body:

{
    "token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNjI4MzY3NjQyLCJleHAiOjE2Mjg0NTQwNDJ9.JmX8Z7f1Wd1a7oZ6b4lRyq5L2t5hT7uK8Vc6lFZ2nqf3m7wvXyYkKxQ4wGZsYx8oO9q3sFpLpVrY6n1aXnC2g", // This field stores the JWT token.
    "type": "Bearer", // This field stores the token type.
    "id": 1, // This field stores the id of the user.
    "username": "test", // This field stores the username of the user.
    "email": "[email protected]", // This field stores the email of the user.
    "roles": [ // This field stores a list of role names that belong to the user.
        "user",
        "moderator"
    ]
}

This means that our login endpoint works as expected and our JWT token has been generated and sent back with some user information.

We can copy the JWT token and use it to access other protected endpoints by adding an Authorization header with the value Bearer <token>.

Get All Users Request

We can create a GET request with URL http://localhost:8080/api/users/ and header:

Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNjI4MzY3NjQyLCJleHAiOjE2Mjg0NTQwNDJ9.JmX8Z7f1Wd1a7oZ6b4lRyq5L2t5hT7uK8Vc6lFZ2nqf3m7wvXyYkKxQ4wGZsYx8oO9q3sFpLpVrY6n1aXnC2g

This request will get a list of all users in the database.

We can send this request and check the response. We should get a status code of 200 OK and a body:

[
    {
        "id": 1,
        "username": "test",
        "password": "$2a$10$HcDkPdHbBtWvTbDvSdRgAeBzPcSfWbAeHr6PfKkHlBtDkRqWvTbDvS",
        "email": "[email protected]",
        "roles": [
            {
                "id": 1,
                "name": "user",
                "users": null
            },
            {
                "id": 2,
                "name": "moderator",
                "users": null
            }
        ]
    }
]

This means that our get all users endpoint works as expected and our list of user entities has been returned with their details.

Note that we can only access this endpoint if we have the role of ‘admin’, otherwise we will get a status code of 403 Forbidden and a body:

Error: Forbidden

Add Role to User Request

We can create another POST request with URL http://localhost:8080/api/users/add-role/test/admin and header:

Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNjI4MzY3NjQyLCJleHAiOjE2Mjg0NTQwNDJ9.JmX8Z7f1Wd1a7oZ6b4lRyq5L2t5hT7uK8Vc6lFZ2nqf3m7wvXyYkKxQ4wGZsYx8oO9q3sFpLpVrY6n1aXnC2g

This request will add the role of ‘admin’ to the user with username ‘test’.

We can send this request and check the response. We should get a status code of 200 OK and a body:

Role added successfully to user!

This means that our add role to user endpoint works as expected and our user entity has been updated with the new role.

Conclusion

In this blog post, we have learned how to create a Spring Boot app with JWT authentication and PostgreSQL. We have used Spring Boot, Spring Security, Spring Data JPA, PostgreSQL, Lombok, and JWT to implement the features and functionality of our app. We have also used Postman to test our endpoints and check the results.

We have covered the following steps:

  • Create a Spring Boot project
  • Configure the application properties
  • Create the entity classes
  • Create the repository interfaces
  • Create the service classes
  • Create the DTO classes
  • Create the controller classes
  • Create the security classes
  • Test the application

A working application can be found here: https://github.com/RedGhoul/springboot-jwt-postgres

I hope you have enjoyed this blog post and learned something new. Thank you for reading! 😊

Leave a Reply

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