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 AuthenticationEntryPoint
interface 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! 😊