Introduction
In this tutorial, we will implement token-based authentication using Spring Boot, Spring Security, JWT (JSON Web Token), and MySQL database. This approach provides a secure and efficient way to handle user authentication in your Spring Boot applications.
JWT (JSON Web Token) Overview
What is JWT?
JWT stands for JSON Web Token, an open standard for securely transmitting information as a JSON object between parties. It is a compact, self-contained method of transmitting data between a client and a server. JWTs are commonly used for authentication and authorization purposes.
Structure of JWT
A JWT consists of three parts:
- Header: Contains metadata about the type of token and the algorithm used to sign the token.
- Payload: Contains claims about the user or entity being authenticated. These claims can include information such as the user ID, username, or email address.
- Signature: Generated using a secret key and the header and payload, ensuring the integrity of the JWT.
Advantages of JWT
One advantage of using JWTs is that they are stateless, meaning the server does not need to keep track of the user’s authentication state. This leads 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
What is Spring Security?
Spring Security is a framework that provides authentication, authorization, and protection against common attacks. It is the de-facto standard for securing Spring-based applications, with support for both web and reactive applications. Using Spring Security with JWT enhances the security of your applications by adding robust authentication mechanisms.
Database Setup
First, create a database in the MySQL server using the following command:
create database login_system;
Add Maven Dependencies
Add the following Maven dependencies to your Spring Boot project to integrate Spring Security, JWT, and MySQL:
<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>
<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-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
Configure MySQL Database
Open the src/main/resources/application.properties
file and add the following properties to configure the database connection:
spring.datasource.url=jdbc:mysql://localhost:3306/login_system
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update
logging.level.org.springframework.security=DEBUG
app.jwt-secret=daf66e01593f61a15b857cf433aae03a005812b31234e149036bcc8dee755dbb
app.jwt-expiration-milliseconds=604800000
Model Layer – Create JPA Entities
User Entity
Create a User
entity class that maps to the users
table in the database:
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;
}
This class represents a user with fields for ID, name, username, email, password, and roles. It uses JPA annotations to map the fields to the corresponding database columns.
Role Entity
Create a Role
entity class that maps to the roles
table in the database:
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;
}
This class represents a role with fields for ID and name. It uses JPA annotations to map the fields to the corresponding database columns.
Repository Layer
UserRepository
Create a UserRepository
interface for performing CRUD operations on the User
entity:
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);
}
This interface provides methods to find a user by username, check if an email exists, find a user by username or email, and check if a username exists.
RoleRepository
Create a RoleRepository
interface for performing CRUD operations on the Role
entity:
import net.javaguides.todo.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}
This interface provides a method to find a role by its name.
JWT Implementation
JwtAuthenticationEntryPoint
Create a JwtAuthenticationEntryPoint
class that handles unauthorized access attempts:
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());
}
}
This class implements AuthenticationEntryPoint
and overrides the commence
method to send an HTTP 401 Unauthorized error response when an authentication exception occurs.
JWT – application.properties Change
Add the following JWT-related properties to the application.properties file:
app.jwt-secret=daf66e01593f61a15b857cf433aae03a005812b31234e149036bcc8dee755dbb
app.jwt-expiration-milliseconds=604800000
JTW Utility Class – JwtTokenProvider.java
Create a JwtTokenProvider
class that provides methods for generating, validating, and extracting information from JWTs:
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;
}
Explanation: 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;
}
Explanation: The 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;
}
Explanation: The 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
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;
}
}
Explanation: This 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
Create a CustomUserDetailsService
class that implements the UserDetailsService
interface and provides an implementation for the loadUserByUsername
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
);
}
}
Explanation: 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
.
The CustomUserDetailsService
class is annotated with @Service
to indicate that it is a Spring service and can be automatically discovered by the Spring context.
Spring Security Configuration
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();
}
}
Explanation: 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
LoginDto Class
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;
}
JWTAuthResponse Class
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;
}
}
Explanation: 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.
Controller Layer – Login REST API return JWT Token
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);
}
}
Explanation: 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, use the following SQL scripts to insert data into the respective tables:
INSERT INTO `users` VALUES
(1,'ramesh@gmail.com','ramesh','$2a$10$5PiyN0MsG0y886d8xWXtwuLXK0Y7zZwcN5xm82b4oDSVr7yF0O6em','ramesh'),
(2,'admin@gmail.com','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 screenshot below 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. This approach ensures a secure and scalable authentication mechanism for your Spring Boot applications.
Sir can you please provide how to setup Kafka with two different microservices with docker compose based connection within the local environment also in the docker containers of those microservices I’ll be really thankful to you ❤