Spring Boot + Spring Security + JWT + MySQL Database Tutorial

In this tutorial, we will learn how to implement token-based authentication using Spring Boot, Spring Security, JWT, and MySQL database.

JWT (JSON Web Token) Overview

JWT stands for JSON Web Token, which is an open standard for securely transmitting information as a JSON object between parties. It is a compact, self-contained method of transmitting data between two parties, typically a client and a server. 

JWTs are often used for authentication and authorization purposes, as they allow a server to authenticate a user by verifying the digital signature contained within the JWT. 

A JWT consists of three parts: a header, a payload, and a signature.  

  • The header contains metadata about the type of token and the algorithm used to sign the token. 
  • The payload contains the claims, or statements, about the user or entity being authenticated. These claims can include information such as the user ID, username, or email address. 
  • The signature is generated using a secret key and the header and payload, ensuring the integrity of the JWT.  

One advantage of using JWTs is that they are stateless, meaning that the server does not need to keep track of the user's authentication state. This can lead to improved scalability and performance. Additionally, JWTs can be used across different domains and services, as long as they share the same secret key for verifying the signature.

Spring Security Overview

Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first-class support for securing both web and reactive applications, it is the de-facto standard for securing Spring-based applications. 

Spring Security is used to secure web applications, REST APIs, and Microservices. 

Spring Security provides built-in support for both authentication and authorization.

Database Set up

Add Maven Dependencies

Add below Maven dependencies to your Spring Boot project:
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>0.11.5</version>
			<scope>runtime</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>0.11.5</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId>
			<version>0.11.5</version>
			<scope>runtime</scope>
		</dependency>

Configure MySQL Database

Let's first create a database in MySQL server using the below command:
create database login_system
Since we’re using MySQL as our database, we need to configure the database URLusername, and password so that Spring can establish a connection with the database on startup. Open the src/main/resources/application.properties file and add the following properties to it:
spring.datasource.url = jdbc:mysql://localhost:3306/login_system
spring.datasource.username = root
spring.datasource.password = root

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

logging.level.org.springframework.security=DEBUG

Model Layer - Create JPA Entities 

In this step, we will create User and Role JPA entities and establish MANY-to-MANY relationships between them. Let's use JPA annotations to establish MANY-to-MANY relationships between User and Role entities.

User

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

import java.util.Set;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    @Column(nullable = false, unique = true)
    private String username;
    @Column(nullable = false, unique = true)
    private String email;
    @Column(nullable = false)
    private String password;

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

Role

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

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

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

Repository Layer

Next, let's create a repository package. Within a repository package, we create UserRepository and RoleRepository interfaces.

UserRepository

import net.javaguides.todo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByUsername(String username);

    Boolean existsByEmail(String email);

    Optional<User> findByUsernameOrEmail(String username, String email);

    boolean existsByUsername(String username);
}

RoleRepository

import net.javaguides.todo.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Map;
import java.util.Optional;

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

JWT Implementation

Create a security package in your Spring boot project and add the following JWT-related classes.

JwtAuthenticationEntryPoint

Let's create a JwtAuthenticationEntryPoint class and add the following code to it:

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}

AuthenticationEntryPoint is used by ExceptionTranslationFilter to commence an authentication scheme. It is the entry point to check if a user is authenticated and logs the person in or throws an exception (unauthorized). Usually, the class can be used like that in simple applications but when using Spring security in REST, JWT, etc one will have to extend it to provide better Spring Security filter chain management.

JWT - application.properties Change

Let's add below two JWT-related properties to the application.properties file:
app.jwt-secret=daf66e01593f61a15b857cf433aae03a005812b31234e149036bcc8dee755dbb
app-jwt-expiration-milliseconds=604800000

JTW Utility Class - JwtTokenProvider.java

Let's create a Utility class named JwtTokenProvider which provides methods for generating, validating, and extracting information from JSON Web Tokens (JWTs) used for authentication in a Spring Boot application.
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtTokenProvider {

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

    @Value("${app.jwt-secret}")
    private String jwtSecret;

    @Value("${app-jwt-expiration-milliseconds}")
    private long jwtExpirationDate;

    // generate JWT token
    public String generateToken(Authentication authentication){
        String username = authentication.getName();

        Date currentDate = new Date();

        Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);

        String token = Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expireDate)
                .signWith(key())
                .compact();
        return token;
    }

    private Key key(){
        return Keys.hmacShaKeyFor(
                Decoders.BASE64.decode(jwtSecret)
        );
    }

    // get username from Jwt token
    public String getUsername(String token){
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key())
                .build()
                .parseClaimsJws(token)
                .getBody();
        String username = claims.getSubject();
        return username;
    }

    // validate Jwt token
    public boolean validateToken(String token){
        try{
            Jwts.parserBuilder()
                    .setSigningKey(key())
                    .build()
                    .parse(token);
            return true;
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            logger.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            logger.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty: {}", e.getMessage());
        }
        return false;
    }
}

generateToken Method

    public String generateToken(Authentication authentication){
        String username = authentication.getName();

        Date currentDate = new Date();

        Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);

        String token = Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expireDate)
                .signWith(key())
                .compact();
        return token;
    }
The generateToken(Authentication authentication) method generates a new JWT based on the provided Authentication object, which contains information about the user being authenticated. It uses the Jwts.builder() method to create a new JwtBuilder object, sets the subject (i.e., username) of the JWT, the issue date, and expiration date, and signs the JWT using the key() method. Finally, it returns the JWT as a string. 

getUsername(String token)

    // get username from Jwt token
    public String getUsername(String token){
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key())
                .build()
                .parseClaimsJws(token)
                .getBody();
        String username = claims.getSubject();
        return username;
    }
getUsername(String token) method extracts the username from the provided JWT. It uses the Jwts.parserBuilder() method to create a new JwtParserBuilder object, sets the signing key using the key() method and parses the JWT using the parseClaimsJws() method. It then retrieves the subject (i.e., username) from the JWT's Claims object and returns it as a string. 

validateToken(String token)

    // validate Jwt token
    public boolean validateToken(String token){
        try{
            Jwts.parserBuilder()
                    .setSigningKey(key())
                    .build()
                    .parse(token);
            return true;
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token: {}", e.getMessage());
        } catch (ExpiredJwtException e) {
            logger.error("JWT token is expired: {}", e.getMessage());
        } catch (UnsupportedJwtException e) {
            logger.error("JWT token is unsupported: {}", e.getMessage());
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty: {}", e.getMessage());
        }
        return false;
    }
validateToken(String token) method validates the provided JWT. It uses the Jwts.parserBuilder() method to create a new JwtParserBuilder object, sets the signing key using the key() method and parses the JWT using the parse() method. If the JWT is valid, the method returns true. If the JWT is invalid or has expired, the method logs an error message using the logger object and returns false.

JwtAuthenticationFilter

Let's create a JwtAuthenticationFilter class in a Spring Boot application that intercepts incoming HTTP requests and validates JWT tokens that are included in the Authorization header. If the token is valid, the filter sets the current user's authentication in the SecurityContext.
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private JwtTokenProvider jwtTokenProvider;

    private UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {
        this.jwtTokenProvider = jwtTokenProvider;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        // get JWT token from http request
        String token = getTokenFromRequest(request);

        // validate token
        if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)){

            // get username from token
            String username = jwtTokenProvider.getUsername(token);

            // load the user associated with token
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                userDetails,
                null,
                userDetails.getAuthorities()
            );

            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        }

        filterChain.doFilter(request, response);
    }

    private String getTokenFromRequest(HttpServletRequest request){

        String bearerToken = request.getHeader("Authorization");

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

        return null;
    }
}
Here's a breakdown of the key parts of the above code:
  • The class extends the Spring framework's OncePerRequestFilter, which ensures that the filter is only applied once per request.
  • The constructor takes two dependencies: JwtTokenProvider and UserDetailsService, which are injected via Spring's constructor dependency injection mechanism.
  • The doFilterInternal method is the main logic of the filter. It extracts the JWT token from the Authorization header using the getTokenFromRequest method, validates the token using the JwtTokenProvider class, and sets the authentication information in the SecurityContextHolder.
  • The getTokenFromRequest method parses the Authorization header and returns the token portion.
  • The SecurityContextHolder is used to store the authentication information for the current request. In this case, the filter sets a UsernamePasswordAuthenticationToken with the UserDetails and authorities associated with the token.

CustomUserDetailsService

Let's write a logic to load user details by name or email from the database. 

Let's create a CustomUserDetailsService class that implements the UserDetailsService interface ( Spring security in-build interface) and provides an implementation for the loadUserByUername() method:
import lombok.AllArgsConstructor;
import net.javaguides.todo.entity.User;
import net.javaguides.todo.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

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

@Service
@AllArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {

        User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
                .orElseThrow(() -> new UsernameNotFoundException("User not exists by Username or Email"));

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

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

Spring Security uses the UserDetailsService interface, which contains the loadUserByUsername(String username) method to look up UserDetails for a given username

The UserDetails interface represents an authenticated user object and Spring Security provides an out-of-the-box implementation of org.springframework.security.core.userdetails.User

Spring Security Configuration

Let's create a class SpringSecurityConfig and add the following configuration to it:
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@AllArgsConstructor
public class SpringSecurityConfig {

    private UserDetailsService userDetailsService;

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

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .authorizeHttpRequests((authorize) -> {
                      authorize.requestMatchers("/api/auth/**").permitAll();
                    authorize.anyRequest().authenticated();
                });
        return http.build();
    }

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

The @Configuration annotation indicates that this class defines a configuration for the Spring application context. 

The @AllArgsConstructor annotation is from the Lombok library and it generates a constructor with all the fields that are annotated with @NonNull. 

The passwordEncoder() method is a bean that creates a BCryptPasswordEncoder instance for encoding passwords. 

The securityFilterChain() method is a bean that defines the security filter chain. The HttpSecurity parameter is used to configure the security settings for the application. In this case, the method disables CSRF protection and authorizes requests based on their HTTP method and URL. 

The authenticationManager() method is a bean that provides an AuthenticationManager. It retrieves the authentication manager from the AuthenticationConfiguration instance.

DTO Layer

Let's create a LoginDto class and add the following content to it:
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {
    private String usernameOrEmail;
    private String password;
}
Next, let's create a JWTAuthResponse class and add the following code to it:
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class JWTAuthResponse {
    private String accessToken;
    private String tokenType = "Bearer";
}

Service Layer

Create a service package and add the following service layer-related AuthService interface and AuthServiceImpl class.

AuthService Interface

import net.javaguides.todo.dto.LoginDto;

public interface AuthService {
    String login(LoginDto loginDto);
}

AuthServiceImpl Class

import net.javaguides.todo.dto.LoginDto;
import net.javaguides.todo.repository.RoleRepository;
import net.javaguides.todo.repository.UserRepository;
import net.javaguides.todo.security.JwtTokenProvider;
import net.javaguides.todo.service.AuthService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

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

@Service
public class AuthServiceImpl implements AuthService {

    private AuthenticationManager authenticationManager;
    private UserRepository userRepository;
    private PasswordEncoder passwordEncoder;
    private JwtTokenProvider jwtTokenProvider;


    public AuthServiceImpl(
            JwtTokenProvider jwtTokenProvider,
            UserRepository userRepository,
            PasswordEncoder passwordEncoder,
            AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public String login(LoginDto loginDto) {

        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
                loginDto.getUsernameOrEmail(), loginDto.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String token = jwtTokenProvider.generateToken(authentication);

        return token;
    }
}

This is the implementation of the AuthService interface. It contains a single method, login(), that handles the login functionality of the application. The loginDto object contains the username and password entered by the user. 

The constructor of this class takes four arguments: JwtTokenProvider, UserRepository, PasswordEncoder, and AuthenticationManager

In the login() method, the authenticationManager attempts to authenticate the user by passing their loginDto credentials to the UsernamePasswordAuthenticationToken. If the authentication is successful, a token is generated using the jwtTokenProvider object and returned to the caller. 

This service class is annotated with @Service to indicate that it is a Spring service and can be automatically discovered by the Spring context.

Controller Layer - Login REST API return JWT Token

Let's create an AuthController class and add the following code to it:

import lombok.AllArgsConstructor;
import net.javaguides.todo.dto.JWTAuthResponse;
import net.javaguides.todo.dto.LoginDto;
import net.javaguides.todo.service.AuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

    private AuthService authService;

    // Build Login REST API
    @PostMapping("/login")
    public ResponseEntity<JWTAuthResponse> authenticate(@RequestBody LoginDto loginDto){
        String token = authService.login(loginDto);

        JWTAuthResponse jwtAuthResponse = new JWTAuthResponse();
        jwtAuthResponse.setAccessToken(token);

        return ResponseEntity.ok(jwtAuthResponse);
    }
}

This code defines a REST API endpoint for user authentication. It receives a POST request at the "/api/auth/login" URL with the login credentials in the request body as a JSON object. The LoginDto object is used to map the JSON object to a Java object.

The AuthController class has a constructor that receives an instance of AuthService, which provides the authentication logic.

The authenticate method receives the LoginDto object as a parameter, and it calls the login method of the AuthService to perform the authentication. The login method returns a JWT token if the authentication is successful. The token is then wrapped in a JWTAuthResponse object and returned as a response.

The @PostMapping annotation maps the method to the HTTP POST method. The @RequestBody annotation indicates that the request body should be mapped to the LoginDto object.

Insert SQL Scripts

Before testing Spring security and JWT, make sure that you use below SQL scripts to insert the database into respective tables:
INSERT INTO `users` VALUES
(1,'[email protected]','ramesh','$2a$10$5PiyN0MsG0y886d8xWXtwuLXK0Y7zZwcN5xm82b4oDSVr7yF0O6em','ramesh'),
(2,'[email protected]','admin','$2a$10$gqHrslMttQWSsDSVRTK1OehkkBiXsJ/a4z2OURU./dizwOQu5Lovu','admin');

INSERT INTO `roles` VALUES (1,'ROLE_ADMIN'),(2,'ROLE_USER');

INSERT INTO `users_roles` VALUES (2,1),(1,2);
Hibernate will automatically create the database tables so you don't need to create the tables manually.

Testing using Postman

Refer to the below screenshot to test the Login REST API that returns the JWT token in the response:

Conclusion

In this tutorial, we learned how to implement token-based authentication using Spring Boot, Spring Security, JWT, and MySQL database.

Related Tutorials

Comments