Step-by-Step JWT-Based Authentication and Authorization Processes with Spring Security
In the computer age, the increase in data and information has created a critical requirement for ensuring the protection of information. Research shows that the amount of data and information produced in the digital age has surpassed the total amount of data produced throughout human history.

-Distribution of data volume by years-
With the development of the Web, dynamic web pages have replaced static single-page websites. As a result of this transformation, the need to secure access to information resources has arisen, and our accesses have been brought under control through certain security frameworks. These security frameworks have been established to protect information resources from unauthorized users or systems.
In this article, we will examine how we can monitor and regulate access to our services and information resources using Spring Security, the standard for security in Spring projects. In addition, we will discuss how these monitoring mechanisms can be effectively configured through a simple example, and by applying these improved methods, we will protect our services from unauthorized access.
Before we start developing our application;
Let us answer the question: What Are the Basic Security Requirements of a Website?
Before moving on to configurations, let us examine what constitutes the security requirements of a website.
The basic security needs of a website consist of the following components:
Authentication
Authorization
Data Encryption
Logging
Firewall, IDS, and IPS Systems
DDoS Protection
Software Updates
Database Security
Error Management
Backup Solutions
Access Control Lists (ACLs)
Security Measures Against Attacks like XSS, CSRF, etc.
To meet our security needs, in this article we will examine how authentication and authorization processes are carried out using Spring Security.
1- Authentication Processes in Spring Boot

In general, the authentication lifecycle includes the steps mentioned above. However, the components used may vary depending on the scenario you implement.
1-HttpRequest
The authentication process begins when the user sends an HTTP request (HttpRequest) to access a protected resource in the application. This usually occurs via a login form (submission of username and password) or through an API request.
2-Authentication Filter
First, the user's request passes through an Authentication Filter.
For example: UsernamePasswordAuthenticationFilter captures the username and password entered by the user.
In a token-based system, this filter verifies a JWT (JSON Web Token) or another type of token.
Then, it creates a UsernamePasswordAuthenticationToken object containing the user's login information.
3-Authentication Token
At this stage, the authentication token created by the filter (for example, UsernamePasswordAuthenticationToken ) is forwarded to Spring Security’s Authentication Manager component. This token contains credentials such as username and password.
4-Authentication Manager
This component, which acts as a central router, selects the appropriate Authentication Provider to verify the authentication token.
For example, if an application uses both DaoAuthenticationProvider and LdapAuthenticationProvider, the Authentication Manager determines which provider to use for the token.
5 - Authentication Provider
The authentication process is carried out at this stage: The selected Authentication Provider performs the actual authentication.
Multiple providers can be configured.
For example:
DaoAuthenticationProvider: Verifies user credentials via a database (for example, using JDBC or JPA).
LdapAuthenticationProvider: Authenticates users via an LDAP (Lightweight Directory Access Protocol) server.
CasAuthenticationProvider: Integrates authentication with the CAS (Central Authentication Server) protocol.
6-UserDetailsService
If DaoAuthenticationProvider is used, UserDetailsService is called to retrieve user information:
MemoryUserDetailsService: Used when users are stored in memory.
CustomUserDetailsService: Retrieves user information from a database or another data source.
After user information is successfully retrieved, a UserDetails object is returned.
7-UserDetails
The UserDetails object contains the following information:
Username
Password
Roles assigned to the user (for example: ROLE_USER, ROLE_ADMIN)
8 - Authentication Provider (continued)
After the user information is successfully verified, the Authentication Provider returns the authentication result to the Authentication Manager.
9 - Provider Manager
The result obtained is evaluated by the Provider Manager:
If authentication is successful, an Authentication object is created.
If authentication fails, an exception such as BadCredentialsException is thrown.
Throughout the entire lifecycle of the application, user credentials are stored within the SecurityContext.
This context is usually managed by accessing it through SecurityContextHolder.
General Workflow:
The user sends a request.
The Authentication Filter creates an authentication token and forwards it to the Authentication Manager.
The Authentication Manager selects the appropriate Authentication Provider.
The Authentication Provider verifies the user via UserDetailsService.
After successful authentication, user information is stored within the Security Context.
This flow is generally implemented in Spring Boot applications by including the spring-boot-starter-security dependency.
When customization is required, components such as UserDetailsService, AuthenticationProvider, or AuthenticationFilter can be customized (by redefining them).
Authentication Mechanisms in Spring Security
Spring Security offers a rich and comprehensive set of authentication mechanisms.
As of 2023, SpriThe main authentication mechanisms supported by Spring Security are as follows:
General Authentication Mechanisms:
Username and Password
OAuth 2.0 Login
SAML 2.0 Login
Central Authentication Server (CAS)
Remember Me
JAAS (Java Authentication and Authorization Service) Authentication
OpenID
Pre-Authentication Scenarios
X509 Authentication (X.509 Certificate-Based Authentication)
Username and Password-Based Authentication Methods
Form Login:
This method provides a dedicated interface where users can enter their username and password information. The authentication process is carried out through this user-friendly login form.
Basic Authentication:
Also known as HTTP Basic Authentication, in this method, the username and password are encoded in Base64 format and sent along with the HTTP request. Due to its simplicity, it is often used in API integrations.
Digest Authentication:
In this method, HTTP Digest Authentication creates a hash using the username, password, and additional information provided by the server. Authentication is performed through this hash, making it more secure than Basic Authentication.
JDBC Authentication:
Suitable for scenarios where user credentials are stored in the database. During authentication, the system interacts with the database to verify the user's information.
LDAP Authentication:
This method, based on the Lightweight Directory Access Protocol (LDAP), works in integration with LDAP servers where user and authorization information is centrally stored in organizations. Authentication is performed directly through the LDAP server.
We will continue our article with a Spring Boot application example based on username and password-based authentication.
In our application, JWT-based authentication and authorization mechanisms will be used. With this application, it is aimed to provide secure access to APIs created to represent the administrative operations of a sales table.
Users who register and log in to the system can send requests to product tables via JWT token.
In this way, they can access the relevant information by accessing product and sales lists. If the credentials are incorrect, informative error messages are displayed to the user. In addition, if the token has expired, access to the relevant tables is blocked, thus ensuring the security of the system.
A voice from the kitchen: Our project was developed on the Spring Boot framework. API documentation was created using Swagger; request tests were carried out using Postman.
MySQL was used as the database, and database modules were visualized with DBeaver.
Application Workflow:
User Registration → Login → Operations with JWT
User Registration
An API endpoint (/register) is created to add users to the system.
Information such as username and password is collected from the user.
The password is encrypted using BCryptPasswordEncoder and securely stored in the database
Login with a Registered User
The user submits their username and password during login.
If the login is successful, a JWT token is issued to the user.
With this token, the user can access protected endpoints.
Operations with JWT
The user adds the JWT token to the Authorization header in their requests.
After the token is validated and the user's identity is confirmed, access to the endpoint is granted.
Defining our application based on layered architecture will make the system more understandable by strengthening structural integrity and semantic clarity.
1-Entities (User, Product, Sale)
2-Repository (UserRepository, ProductRepository, SaleRepository)
3-Service (ProductService, UserService)
4-Controller (AuthController, ProductController)
5-Configuration (SecurityConfig, JwtAuthenticationFilter)
6-Domain (LoginRequest, RegisterRequest)
7-Util (JwtTokenUtil)
The Package Hierarchy of Our Application is as Follows:

The Dependencies Used in Our Project are as Follows:
<dependencies>
<!-- Spring Boot Starter Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Devtools for Development -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Database Dependency -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT Dependencies -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version> <!-- or latest -->
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version> <!-- or latest -->
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version> <!-- or latest -->
<scope>runtime</scope>
</dependency>
<!-- Lombok for Boilerplate Code -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Swagger Dependencies-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>The list form of our dependencies is as follows;
spring-boot-starter-data-jpa
spring-boot-starter-security
spring-boot-starter-web
spring-boot-devtools
spring-boot-starter-test
spring-security-test
mysql-connector-j
jjwt-api
jjwt-impl
jjwt-jackson
lombok
springdoc-openapi-starter-webmvc-ui
In our application's properties file, database connection definitions, JPA configurations, and Swagger settings are included.
spring.application.name=com.timeissecuritytime
spring.datasource.url=jdbc:mysql://localhost:3307/springsecurityexamples?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=test1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.api-docs.path=/v3/api-docsIf we proceed according to the Layered architecture model I shared above, I will first share the entity classes and the corresponding SQL codes.
User (Entity)
package com.timeissecuritytime.entity;
import lombok.*;
import jakarta.persistence.*;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
}The SQL code we will use in our database for our Entity is as follows;
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf16;The User table is designed to store the username and password information of users registered in the system.
This table is directly related to the DTO (Data Transfer Object) and entity structures used within the application.
This relationship between the database table and DTOs ensures seamless data flow between application layers; thus, it becomes possible to efficiently map database records to objects used in the service and controller layers.
Product (Entity)
package com.timeissecuritytime.entity;
import lombok.Data;
import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
@Entity
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productName;
private String productDescription;
private Double price;
private Integer stockQuantity;
}The SQL code we will use in our database for our Product Entity is as follows;
CREATE TABLE `product` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`product_name` VARCHAR(255) NOT NULL,
`product_description` TEXT,
`price` DOUBLE NOT NULL,
`stock_quantity` INT NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Sale (Entity)
package com.timeissecuritytime.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Data;
@Entity
@Data
public class Sale {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
private Integer saleQuantity;
private java.util.Date saleDate;
}The SQL code we will use in our database for our Sale Entity is as follows;
CREATE TABLE `sale` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`product_id` BIGINT NOT NULL,
`sale_quantity` INT NOT NULL,
`sale_date` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`),
CONSTRAINT `sales_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;The Product and Sale entities and db objects are not the main topic of our article. These entities were created only to demonstrate some endpoints accessible by logged-in users.
With this example, it is aimed to practically demonstrate the use of JWT and to emphasize access control mechanisms through the created services.
In this context, ProductRepository, UserRepository, and SaleRepository have been created for our application.
package com.timeissecuritytime.repository;
import com.timeissecuritytime.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}package com.timeissecuritytime.repository;
import com.timeissecuritytime.entity.Sale;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface SaleRepository extends JpaRepository<Sale, Long> {
List<Sale> findByProductId(Long productId);
}package com.timeissecuritytime.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.timeissecuritytime.entity.User;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}You can create sample data instances for the Product and Sale tables by running the following SQL scripts:
INSERT INTO `product` (`product_name`, `product_description`, `price`, `stock_quantity`)
VALUES
('Laptop', 'High-performance gaming laptop', 1500.00, 10),
('Smartphone', 'Latest model smartphone with advanced features', 999.99, 20);
INSERT INTO `sale` (`product_id`, `sale_quantity`, `sale_date`)
VALUES
(1, 2, '2025-01-28 12:00:00'),
(2, 1, '2025-01-28 13:30:00');Now, we proceed to the definition of our project's service files.
UserService
package com.timeissecuritytime.service;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import com.timeissecuritytime.entity.User;
import com.timeissecuritytime.repository.UserRepository;
import com.timeissecuritytime.Constants;
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public User registerUser(String username, String password) {
User user = new User();
user.setUsername(username);
user.setPassword(bCryptPasswordEncoder.encode(password));
return userRepository.save(user);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(Constants.USER_NOT_FOUND + username));
return org.springframework.security.core.userdetails.User
.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles("USER")
.build();
}
public User authenticateUser(String username, String password) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(Constants.USER_NOT_FOUND + username));
if (!bCryptPasswordEncoder.matches(password, user.getPassword())) {
throw new IllegalArgumentException(Constants.INVALID_PASSWORD);
}
return user;
}
}The Basic Methods in the UserService Class and Their Technical Functions are as follows;
1-registerUser(String username, String password)
This method is used to register a new user in the system.
Technical flow:
It receives the incoming username and password parameters.
A User object is created and the data from the parameters is assigned to the object.
The password is encrypted (hashed) using BCryptPasswordEncoder and set to the User object.
The user object is saved to the database via the userRepository.save(user) method.
In summary, it is designed to ensure the secure storage of the user's password and to add a new user to the system.
2-loadUserByUsername(String username)
(Implementation of Spring Security’s UserDetailsService)
It is developed to collect user information and return a UserDetails object compatible with Spring Security’s authentication mechanisms. It is automatically called during Spring Security’s authentication process.
Technical flow:
The user is searched in the database using the userRepository.findByUsername(username) method.
If the user is not found, a UsernameNotFoundException is thrown.
If the user is found, a UserDetails object is created using Spring Security’s User class.
This object contains the username, password, and a default role (USER).
3-authenticateUser(String username, String password)
It is developed to verify the user's login credentials. It is called to manually check the accuracy of user information.
Technical flow:
The user is searched in the database using the userRepository.findByUsername(username) method.
If the user is not found, a UsernameNotFoundException is thrown.
The entered password is compared with the hashed password stored in the database using the BCryptPasswordEncoder.matches method.
If the passwords do not match, an IllegalArgumentException is thrown.
If the verification is successful, the relevant User object is returned.
registerUser: This is the method that helps us register the user. By hashing the user's password, it allows us to store it securely.
loadUserByUsername: Provides us with a UserDetails object for Spring Security’s authentication process.
authenticateUser: Manually verifies user credentials; it is generally preferred in cases where custom authentication logic is required.
These methods are designed to establish the user authentication and authorization infrastructure in our application.
ProductService
package com.timeissecuritytime.service;
import com.timeissecuritytime.entity.Product;
import com.timeissecuritytime.entity.Sale;
import com.timeissecuritytime.repository.ProductRepository;
import com.timeissecuritytime.repository.SaleRepository;
import com.timeissecuritytime.Constants;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
private final SaleRepository saleRepository;
@PreAuthorize("hasRole('USER')")
@Transactional(readOnly = true)
public List<Product> findAllProducts() {
return productRepository.findAll();
}
@Transactional(readOnly = true)
public Product findProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException(Constants.PRODUCT_NOT_FOUND + id));
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public Product saveProduct(Product product) {
return productRepository.save(product);
}
@Transactional(readOnly = true)
@PreAuthorize("hasRole('USER')")
public List<Sale> findSalesByProductId(Long productId) {
return saleRepository.findByProductId(productId);
}
}This service file contains the basic service methods that we will use to operate on product and sales data. These APIs do not constitute the main focus of our article.
Technically, our methods:
findAllProducts(): Lists all products and is accessible only by users with the USER role.
findProductById(Long id): Finds a product by the given ID value. Throws an error if the product is not found.
saveProduct(Product product): Saves a new product and is accessible only by users with the ADMIN role.
findSalesByProductId(Long productId): Lists the sales belonging to a specific product and is accessible only to users with the USER role.
Now, we proceed to the definition of our project's Controller files.
AuthController
package com.timeissecuritytime.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.timeissecuritytime.util.JwtTokenUtil;
import com.timeissecuritytime.domain.LoginRequest;
import com.timeissecuritytime.domain.RegisterRequest;
import com.timeissecuritytime.entity.User;
import com.timeissecuritytime.service.UserService;
import com.timeissecuritytime.Constants;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
private final JwtTokenUtil jwtTokenUtil;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) {
userService.registerUser(registerRequest.getUsername(), registerRequest.getPassword());
return ResponseEntity.ok(Constants.REGISTRATION_SUCCESS);
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
User user = userService.authenticateUser(loginRequest.getUsername(), loginRequest.getPassword());
String token = jwtTokenUtil.generateToken(user.getUsername());
return ResponseEntity.ok(String.format(Constants.TOKEN_RESPONSE, token));
}
}The AuthController class is designed to be used during user registration and login operations.
/register (POST): Receives the registration request from the user and creates a new user via UserService. When the process is successfully completed, it returns a success message.
/login (POST): Verifies the user's login credentials. If authentication is successful, a JWT token is generated using JwtTokenUtil and delivered to the user.
This controller, applis a basic controller created to manage authentication processes.
ProductController
package com.timeissecuritytime.controller;
import com.timeissecuritytime.entity.Product;
import com.timeissecuritytime.entity.Sale;
import com.timeissecuritytime.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping("/all")
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(productService.findAllProducts());
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return ResponseEntity.ok(productService.findProductById(id));
}
@PostMapping("/add")
public ResponseEntity<Product> addProduct(@RequestBody Product product) {
return ResponseEntity.ok(productService.saveProduct(product));
}
@GetMapping("/sales/{productId}")
public ResponseEntity<List<Sale>> getSalesByProductId(@PathVariable Long productId) {
return ResponseEntity.ok(productService.findSalesByProductId(productId));
}
}The ProductController class was created to manage product and sales operations. It is designed to provide access to product and sales data.
Technically, our methods:
/all (GET): Lists all products.
/{id} (GET): Returns the product with the specified ID value.
/add (POST): Adds and saves a new product.
/sales/{productId} (GET): Lists the sales for the specified productId value.
Now, we move on to the definition of our project's configuration files.
JwtAuthenticationFilter
package com.timeissecuritytime.configuration;
import java.io.IOException;
import java.util.Optional;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.timeissecuritytime.service.UserService;
import com.timeissecuritytime.util.JwtTokenUtil;
import com.timeissecuritytime.Constants;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final UserService userService;
public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil, UserService userService) {
this.jwtTokenUtil = jwtTokenUtil;
this.userService = userService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
Optional<String> tokenOptional = extractToken(request);
if (tokenOptional.isPresent()) {
String token = tokenOptional.get();
if (jwtTokenUtil.validateToken(token)) {
setAuthentication(token);
} else {
handleInvalidToken(response);
return;
}
}
} catch (Exception e) {
handleFilterException(response, e);
return;
}
filterChain.doFilter(request, response);
}
private Optional<String> extractToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(Constants.AUTHORIZATION_HEADER))
.filter(header -> header.startsWith(Constants.BEARER_PREFIX))
.map(header -> header.substring(Constants.BEARER_PREFIX.length()));
}
private void setAuthentication(String token) {
String username = jwtTokenUtil.extractUsername(token);
UserDetails userDetails = userService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private void handleInvalidToken(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(Constants.INVALID_TOKEN_MESSAGE);
}
private void handleFilterException(HttpServletResponse response, Exception e) throws IOException {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write(Constants.UNEXPECTED_ERROR_MESSAGE);
}
}The JwtAuthenticationFilter class is a custom filter defined within Spring Security and is responsible for the validation of JWT (JSON Web Tokens) and authentication operations.
Technically, our methods:
doFilterInternal Method:
Extracts the JWT token from the Authorization header in incoming requests.
If the token is valid, it resolves the username using JwtTokenUtil and performs the authentication process.
If the token is invalid or an error occurs, it returns a response with the appropriate HTTP status code and error message.
extractToken Method:
Removes the Bearer prefix from the Authorization header and extracts the token.
setAuthentication Method:
Loads UserDetails information using the username obtained from the token.
Registers the user's authentication information into the SecurityContext, thus ensuring that the user remains authenticated throughout the entire lifecycle of the request.
handleInvalidToken and handleFilterException Methods:
Helps return a response with the appropriate error message and HTTP status code in cases of invalid token or unexpected error.
This class runs once per HTTP request and centrally manages the JWT validation process.
JwtTokenUtil
package com.timeissecuritytime.configuration;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import java.util.Date;
import java.util.Optional;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class JwtTokenUtil {
private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION_TIME = 86400000; // One-day
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(calculateExpirationDate())
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
return parseClaims(token)
.map(this::isTokenValid)
.orElse(false);
}
public String extractUsername(String token) {
return parseClaims(token)
.map(Claims::getSubject)
.orElse(null);
}
private Date calculateExpirationDate() {
return new Date(System.currentTimeMillis() + EXPIRATION_TIME);
}
private Optional<Claims> parseClaims(String token) {
try {
return Optional.of(Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody());
} catch (JwtException e) {
The JwtTokenUtil class is a utility class designed to manage JWT operations (creation, validation, and parsing).
Our detailed explanations regarding the technical functions of the methods in this class are as follows:generateToken(String username) Method:
setSubject(username): Adds the username to the subject (sub) field of the token.
setIssuedAt(new Date()): Sets the creation date of the token.
setExpiration(calculateExpirationDate()): Calculates and adds the expiration date of the token.
signWith(SECRET_KEY, SignatureAlgorithm.HS256): Signs the token with the HS256 algorithm and SECRET_KEY.
compact(): Returns the generated JWT as a string.
Creates a signed JWT containing the username and validity period information.
validateToken(String token) Method:
parseClaims(token): Parses the Claims object within the token.
isTokenValid(claims): Checks whether the token's period is valid.
Returns Optional.empty() if the token is invalid.
Validates the token and returns true if valid, false if invalid.
extractUsername(String token) Method:
parseClaims(token): Parses the Claims object in the token.
Claims::getSubject: Extracts the sub (username) information from the token.
ExpirationDate Method:
System.currentTimeMillis() + EXPIRATION_TIME: Calculates the token's expiration date by adding one day to the current time.
Returns the expiration date as a Date object.
parseClaims(String token) Method:
Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token): Parses the token and extracts the Claims object.
Throws JwtException and returns Optional.empty() if the token is invalid.
Returns all Claims data within the token.
isTokenValid(Claims claims) Method:
claims.getExpiration(): Retrieves the token's expiration date.
!expirationDate.before(new Date()): Checks whether the token has expired.
Returns true if the token is valid, false if expired.
SecurityConfig
package com.timeissecuritytime.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter)
throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/auth/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/v3/api-docs/swagger-config"
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
The SecurityConfig class performs the Spring Security configuration for the application.
Within SecurityConfig, security rules, authentication mechanisms, and encryption settings are defined.
You can access the technical explanation of each component.
1. filterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) Method
This method defines the basic security configuration of the application; it determines which endpoints are protected, which are publicly accessible, and how security filters are applied.
csrf(csrf -> csrf.disable()) -> Disables CSRF (Cross-Site Request Forgery) protection.
authorizeHttpRequests(auth -> auth…) -> Used to configure access rules for HTTP endpoints.
requestMatchers(…): Used to specify endpoints that are public and do not require authentication.
/api/auth/ → Made publicly accessible for endpoints used in authentication processes (login, register).
/v3/api-docs/, /swagger-ui/ → Similarly, Swagger API documentation is left open.
anyRequest().authenticated(): Used to require authentication for all other APIs except the permitted endpoints.
addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
Used to include the custom JwtAuthenticationFilter component in Spring Security’s filter chain.
Additionally, this filter intercepts every HTTP request coming to the application and validates the JWT token in the Authorization header.
If the token is valid, it places the user's credentials into the SecurityContext and allows the request to proceed as an authenticated user.
This process occurs before Spring Security’s default UsernamePasswordAuthenticationFilter, thus prioritizing the JWT-based authentication mechanism.
sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
Used to configure session management. Sets the session creation policy to STATELESS; thus, no session is created or maintained on the server.
This approach is required in stateless security architectures such as JWT-based authentication systems. Our method returns a fully configured SecurityFilterChain object that defines how Spring Security will handle incoming requests.
2. passwordEncoder() Method
Our method defines the encoder to be used for hashing and verifying passwords.
Returns a strong BCryptPasswordEncoder instance for securely storing passwords.
Plaintext passwords are never stored; instead, they are saved as hashed values.
Summary of Basic Concepts in the SecurityConfig Class
Stateless Security: The application uses JWT tokens for authentication; therefore, no session state is maintained on the server side. This is ensured by the SessionCreationPolicy.STATELESS configuration.
Filter Chain: Custom filters such as JwtAuthenticationFilter are integrated into the Spring Security filter chain to perform JWT validation operations.
Endpoint Security: General endpoints such as /api/auth/** and /swagger-ui/** are explicitly permitted for access. All other endpoints require authentication.
Password Encryption: Passwords are encrypted using BCryptPasswordEncoder, ensuring secure storage in the database.
Security Flow:
When a request reaches the application, it is processed by the SecurityFilterChain.
If the request matches a public endpoint, it is accepted without authentication.
For all other endpoints, JWT validation is performed and only users with a valid token are granted access.
Below are our files where constants are defined, the classes used for global error handling, and domain objects.
Our domain objects:
package com.timeissecuritytime.domain;
import lombok.Data;
@Data
public class LoginRequest {
private String username;
private String password;
}
package com.timeissecuritytime.domain;
import lombok.Data;
@Data
public class RegisterRequest {
private String username;
private String password;
}
GlobalExceptionHandler;
package com.timeissecuritytime.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
Constants
package com.timeissecuritytime;
public class Constants {
public static final String REGISTRATION_SUCCESS = "User successfully registered!";
public static final String TOKEN_RESPONSE = "Token: %s";
public static final String PRODUCT_NOT_FOUND = "Product not found: ";
public static final String USER_NOT_FOUND = "User not found: ";
public static final String INVALID_PASSWORD = "Invalid password!";
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
public static final String INVALID_TOKEN_MESSAGE = "{\"error\": \"Token has expired or is invalid. Please log in again.\"}";
public static final String UNEXPECTED_ERROR_MESSAGE = "{\"error\": \"An unexpected error occurred.\"}";
}
Now, we will use Postman to test our application.
First, we will create a new user with the register operation.
Then, we will obtain a JWT token using the login operation.
Afterwards, we will try to view the product list without using this token and observe the authorization error we receive.
Finally, we will demonstrate that by providing a valid JWT token, we can successfully access our services and list the products.
1 - User Registration

2 - Login operation and obtaining the token

3 - Attempting to List Products Without a Token and Observing the Error Message

4 - Accessing Services with a Valid Token

After the token is passed to our service, we are able to retrieve the product list

In this article, we have examined in detail the processes of JWT-based authentication and authorization with Spring Security. In addition, we emphasized the importance of building a security-focused infrastructure by designing our application according to a layered architecture.
While explaining the interactions between each component of the application, we used sample codes to make these processes more understandable. This structure will help you develop a secure and sustainable system by preventing unauthorized access.
----
Thank you for reading our article. If you found it useful, please consider sharing it.