Configuring Spring Security With JWT Authentication A Comprehensive Guide

by StackCamp Team 74 views

Hey guys! Today, we're diving deep into the world of securing your Spring Boot applications using Spring Security and JSON Web Tokens (JWT). This guide will walk you through setting up a robust authentication and authorization system, ensuring your backend endpoints are protected and your users are managed effectively. We'll cover everything from adding the necessary dependencies to testing your endpoints with Postman. So, grab your favorite coding beverage, and let's get started!

🎯 Objective: Secure Your Endpoints with Spring Security and JWT

The primary goal here is to implement an authentication and authorization system using Spring Security and JWT. This setup will safeguard your backend endpoints and manage user roles, distinguishing between regular users and administrators. Think of it as building a digital fortress around your application, allowing only the right people with the right credentials to access specific areas. Securing your application is super crucial in today's world, and this combination of Spring Security and JWT is a powerful way to achieve that.

Why Spring Security and JWT?

  • Spring Security: This is a powerful and highly customizable framework for authentication and access control. It provides a comprehensive set of features to handle security in your application.
  • JWT: JSON Web Tokens are a compact, URL-safe means of representing claims to be transferred between two parties. They're digitally signed, ensuring that the claims can be trusted and aren't tampered with. JWTs are perfect for stateless authentication, which is a common requirement for modern web applications and microservices.

By combining these two technologies, you get a secure, scalable, and maintainable authentication system. This setup is especially beneficial for applications that need to handle a large number of users and requests, as JWTs allow you to offload authentication processing from the server.

Key Benefits of this Approach

  • Stateless Authentication: JWTs eliminate the need for server-side sessions, making your application more scalable.
  • Role-Based Access Control: You can define different roles (e.g., user, admin) and grant access to specific endpoints based on these roles.
  • Enhanced Security: JWTs are digitally signed, ensuring the integrity of the token and preventing tampering.
  • Easy Integration: Spring Security provides excellent support for JWTs, making the integration process smooth and straightforward.

📋 Tasks: Your Roadmap to Success

To achieve our objective, we'll break down the process into manageable tasks. Each task is a step closer to a fully functional and secure application. Let's outline the roadmap:

  • [ ] Add Dependencies in pom.xml: We'll start by adding the necessary dependencies to your project's pom.xml file. This includes Spring Security, JWT libraries, and Lombok for simplifying our code.
  • [ ] Create SecurityConfig.java: Next, we'll create a SecurityConfig.java class to configure Spring Security. This class will define the filters, public endpoints, and protected endpoints.
  • [ ] Implement JwtUtil.java: We'll then implement a JwtUtil.java class to handle JWT generation and validation. This utility will be responsible for creating tokens when a user logs in and verifying tokens when a user tries to access a protected resource.
  • [ ] Create JwtAuthenticationFilter.java: A JwtAuthenticationFilter.java will be created to intercept and validate tokens in each request. This filter will live in the request pipeline and check for a valid JWT before allowing access to protected endpoints.
  • [ ] Implement CustomUserDetailsService.java: We'll implement a CustomUserDetailsService.java class to load users from the database. This service will be used by Spring Security to authenticate users based on their credentials.
  • [ ] Create Authentication Endpoints: We need to create authentication endpoints (/login and /register) in the appropriate controller. These endpoints will handle user registration and login, issuing JWTs upon successful authentication.
  • [ ] Test Endpoints with Postman: Finally, we'll test our endpoints using Postman to validate the complete authentication flow. This ensures that everything is working as expected and that our security measures are effective.

Each of these steps is crucial, and we'll go through them in detail to ensure you have a solid understanding of the process. Think of these tasks as building blocks – each one contributes to the overall structure and strength of your security system.

🔧 Technical Requirements: The Building Blocks

Before we dive into the code, let's lay out the technical requirements. This section outlines the technologies and components we'll be using, as well as the specific configurations we need to make.

  • Backend: We're using Spring Boot 3.x, which provides a robust foundation for our application. Spring Security will handle authentication and authorization, and we'll use the io.jsonwebtoken library for JWT functionalities. These technologies work together seamlessly to provide a secure and efficient backend.
  • JWT Library: We'll use the io.jsonwebtoken library for handling JWTs. This library provides methods for generating, parsing, and validating JWTs, making it a crucial part of our authentication process. It simplifies the complexities of working with JWTs and ensures that our tokens are handled securely.
  • Token Inclusions: The tokens must include the user's role. This is essential for implementing role-based access control, allowing us to restrict access to certain endpoints based on the user's role (e.g., admin or user). Including the role in the JWT allows the backend to quickly determine the user's permissions without needing to query the database.
  • Public Endpoints: The /login and /register endpoints will be publicly accessible. This is necessary because users need to be able to register and log in without needing a token. These endpoints are the entry points to our application's security system.
  • Protected Endpoints: Endpoints like /productos, /categorias, and /cotizaciones will be protected. This means that only authenticated users with the appropriate roles will be able to access them. This ensures that sensitive data and functionalities are only available to authorized personnel.
  • Database: We'll use a MySQL database with a usuarios table. This table will store user information, including id, nombre, email, encrypted contraseña, and rol. The database is the backbone of our user management system, providing a persistent store for user credentials and roles.
  • Password Encryption: Passwords will be encrypted using BCryptPasswordEncoder. This is a crucial security measure to prevent passwords from being compromised in case of a data breach. BCrypt is a strong hashing algorithm that makes it extremely difficult for attackers to reverse-engineer passwords.

These technical requirements set the stage for our implementation. Each component plays a vital role in the overall security architecture, ensuring that our application is well-protected and operates smoothly. Understanding these requirements is the first step towards building a secure and reliable system.

✅ Acceptance Criteria: How We'll Know We've Succeeded

To ensure we've met our objective, we need clear acceptance criteria. These criteria will help us validate that our authentication and authorization system is working correctly and securely.

  • Successful Login: A user should be able to log in and receive a valid JWT. This is the fundamental requirement for our authentication system. If a user can't log in and receive a token, nothing else matters.
  • Unauthorized Access: Accessing a protected endpoint without a token should result in a 401 Unauthorized error. This ensures that our endpoints are indeed protected and that only users with valid credentials can access them. This is a critical security measure to prevent unauthorized access to sensitive resources.
  • Token-Based Access: Accessing a protected endpoint with a valid token should grant access based on the user's role. This validates that our role-based access control is working as expected. Users with different roles should have different levels of access to our application.
  • Admin Privileges: Administrators should be able to access restricted endpoints. This ensures that our administrative users have the necessary privileges to manage the application. This is a key aspect of role-based access control.
  • Token Expiration: Tokens should expire after a defined period. This is a crucial security measure to prevent tokens from being used indefinitely if they are compromised. Token expiration limits the window of opportunity for attackers to exploit stolen tokens.

These acceptance criteria provide a clear checklist for verifying the correctness and security of our implementation. Meeting these criteria ensures that our authentication system is robust, reliable, and secure.

Step-by-Step Implementation

Now, let's get our hands dirty with some code! We'll go through each task step-by-step, providing code snippets and explanations along the way. By the end of this section, you'll have a fully functional Spring Security and JWT authentication system.

1. Add Dependencies to pom.xml

First, we need to add the necessary dependencies to our pom.xml file. These dependencies will provide the libraries and frameworks we need for Spring Security, JWT, and Lombok.

<dependencies>
    <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>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <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>
</dependencies>
  • spring-boot-starter-security: Provides the core Spring Security functionalities.
  • spring-boot-starter-web: Provides the necessary dependencies for building web applications with Spring Boot.
  • jjwt-api, jjwt-impl, jjwt-jackson: These are the JWT libraries we'll use to generate and validate tokens.
  • lombok: Lombok helps us reduce boilerplate code, making our classes cleaner and more readable.
  • spring-boot-starter-data-jpa: This dependency is for JPA support, allowing us to interact with the database.
  • mysql-connector-java: This is the MySQL connector, allowing us to connect to our MySQL database.

Make sure to sync your project after adding these dependencies so that Maven can download and install them.

2. Create SecurityConfig.java

Next, we'll create the SecurityConfig.java class. This class will configure Spring Security, defining the filter chain, public endpoints, and protected endpoints.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.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;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }

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

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/login", "/register").permitAll()
                        .requestMatchers("/productos", "/categorias", "/cotizaciones").authenticated()
                        .anyRequest().permitAll()
                )
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}
  • @Configuration: Marks this class as a configuration class.
  • @EnableWebSecurity: Enables Spring Security's web security features.
  • BCryptPasswordEncoder: A bean for encrypting passwords using BCrypt.
  • AuthenticationManager: A bean for managing authentication.
  • SecurityFilterChain: Defines the security filter chain.
    • csrf().disable(): Disables CSRF protection (for simplicity in this example).
    • sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS): Configures the session management to be stateless, as we're using JWT.
    • authorizeHttpRequests(): Configures authorization rules.
      • .requestMatchers("/login", "/register").permitAll(): Allows access to /login and /register endpoints without authentication.
      • .requestMatchers("/productos", "/categorias", "/cotizaciones").authenticated(): Requires authentication for /productos, /categorias, and /cotizaciones endpoints.
      • .anyRequest().permitAll(): Allows all other requests without authentication (you might want to adjust this based on your specific needs).
    • addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class): Adds our JwtAuthenticationFilter before the UsernamePasswordAuthenticationFilter.

This configuration sets up the basic security rules for our application. It defines which endpoints are public and which are protected, and it integrates our JWT authentication filter into the Spring Security filter chain.

3. Implement JwtUtil.java

The JwtUtil.java class will be responsible for generating and validating JWTs. This utility will provide methods for creating tokens when a user logs in and verifying tokens when a user tries to access a protected resource.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Service
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.expiration}")
    private long jwtExpiration;

    @Value("${jwt.refresh-token.expiration}")
    private long refreshExpiration;

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return buildToken(extraClaims, userDetails, jwtExpiration);
    }

    public String generateRefreshToken(UserDetails userDetails) {
        return buildToken(new HashMap<>(), userDetails, refreshExpiration);
    }

    private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) {
        return Jwts.builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    private Claims extractAllClaims(String token) {
        return Jwts
                .parserBuilder()
                .setSigningKey(getSignInKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Key getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}
  • @Service: Marks this class as a service component.
  • @Value: Injects values from the application properties.
    • jwt.secret: The secret key used to sign the JWT.
    • jwt.expiration: The expiration time for the JWT (in milliseconds).
    • jwt.refresh-token.expiration: The expiration time for the refresh token (in milliseconds).
  • extractUsername(String token): Extracts the username from the token.
  • extractClaim(String token, Function<Claims, T> claimsResolver): Extracts a specific claim from the token.
  • generateToken(UserDetails userDetails): Generates a JWT for the given user.
  • generateToken(Map<String, Object> extraClaims, UserDetails userDetails): Generates a JWT with extra claims.
  • generateRefreshToken(UserDetails userDetails): Generates a refresh token for the given user.
  • buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration): Builds the JWT with the given claims, user details, and expiration time.
  • isTokenValid(String token, UserDetails userDetails): Checks if the token is valid for the given user.
  • isTokenExpired(String token): Checks if the token is expired.
  • extractExpiration(String token): Extracts the expiration date from the token.
  • extractAllClaims(String token): Extracts all claims from the token.
  • getSignInKey(): Gets the signing key from the secret key.

This utility class provides all the necessary methods for working with JWTs. It handles token generation, validation, and extraction of claims, making it a crucial component of our authentication system.

4. Create JwtAuthenticationFilter.java

The JwtAuthenticationFilter.java class is a filter that intercepts incoming requests, extracts the JWT from the Authorization header, and validates it. If the token is valid, it sets the authentication context for the user.

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
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 final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        final String token;
        final String username;

        if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        token = authHeader.substring(7);
        username = jwtUtil.extractUsername(token);

        if (StringUtils.hasText(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if (jwtUtil.isTokenValid(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}
  • @Component: Marks this class as a component.
  • OncePerRequestFilter: Ensures that the filter is executed only once per request.
  • doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain): The main method that performs the filtering logic.
    • Extracts the Authorization header from the request.
    • Checks if the header is present and starts with "Bearer ".
    • Extracts the token from the header.
    • Extracts the username from the token.
    • If the username is present and there is no authentication in the security context:
      • Loads the user details from the UserDetailsService.
      • Checks if the token is valid for the user.
      • If the token is valid:
        • Creates an UsernamePasswordAuthenticationToken.
        • Sets the details of the token.
        • Sets the authentication in the security context.
    • Calls filterChain.doFilter(request, response) to continue the filter chain.

This filter is the heart of our JWT authentication system. It intercepts requests, validates tokens, and sets the authentication context, allowing Spring Security to enforce access control rules.

5. Implement CustomUserDetailsService.java

The CustomUserDetailsService.java class implements the UserDetailsService interface. It's responsible for loading user details from the database based on the username.

import org.springframework.security.core.userdetails.User;
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 java.util.ArrayList;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final BCryptPasswordEncoder passwordEncoder;

    public CustomUserDetailsService(BCryptPasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Replace this with your database logic to fetch user details
        if ("user".equals(username)) {
            return new User("user", passwordEncoder.encode("password"), new ArrayList<>());
        }
        throw new UsernameNotFoundException("User not found with username: " + username);
    }
}
  • @Service: Marks this class as a service component.
  • UserDetailsService: Implements the UserDetailsService interface.
  • loadUserByUsername(String username): Loads user details from the database based on the username.
    • Important: Replace the placeholder logic with your actual database interaction code.

This service is crucial for Spring Security to authenticate users. It provides a way to load user details from the database, allowing Spring Security to compare the provided credentials with the stored ones.

6. Create Authentication Endpoints

Now, we need to create the authentication endpoints (/login and /register) in our controller. These endpoints will handle user registration and login, issuing JWTs upon successful authentication.

import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil, UserDetailsService userDetailsService) {
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @PostMapping("/login")
    public ResponseEntity<Map<String, String>> login(@RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
        );

        UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
        String token = jwtUtil.generateToken(userDetails);
        String refreshToken = jwtUtil.generateRefreshToken(userDetails);

        Map<String, String> response = new HashMap<>();
        response.put("token", token);
        response.put("refreshToken", refreshToken);
        return ResponseEntity.ok(response);
    }

    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestBody RegisterRequest request) {
        // Implement your registration logic here
        return ResponseEntity.ok("User registered successfully");
    }

    static class LoginRequest {
        private String username;
        private String password;

        public String getUsername() {
            return username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getPassword() {
            return password;
        }

        public void setPassword(String password) {
            this.password = password;
        }
    }

    static class RegisterRequest {
        // Add registration request fields here
    }
}
  • @RestController: Marks this class as a REST controller.
  • AuthenticationManager: Used to authenticate users.
  • JwtUtil: Used to generate JWTs.
  • UserDetailsService: Used to load user details.
  • @PostMapping("/login"): Handles the login endpoint.
    • Authenticates the user using the AuthenticationManager.
    • Loads the user details using the UserDetailsService.
    • Generates a JWT using the JwtUtil.
    • Returns the token in the response.
  • @PostMapping("/register"): Handles the registration endpoint.
    • Important: Implement your registration logic here.

These endpoints provide the entry points for users to authenticate with our application. The /login endpoint issues JWTs upon successful authentication, and the /register endpoint allows new users to create accounts (you'll need to implement the registration logic).

7. Test Endpoints with Postman

Finally, let's test our endpoints using Postman to validate the complete authentication flow. This ensures that everything is working as expected and that our security measures are effective.

  1. Send a POST request to /login with the username and password.
  2. Verify that you receive a JWT in the response.
  3. Copy the JWT.
  4. Send a GET request to a protected endpoint (e.g., /productos).
  5. Set the Authorization header to Bearer <your_jwt> (replace <your_jwt> with the actual JWT).
  6. Verify that you receive a successful response.
  7. Send a GET request to a protected endpoint without the Authorization header.
  8. Verify that you receive a 401 Unauthorized error.

These tests will confirm that our authentication system is working correctly. You should be able to log in, receive a JWT, access protected endpoints with the JWT, and be denied access without the JWT.

Conclusion: You've Built a Secure Application!

Woohoo! You've made it to the end of this comprehensive guide. By now, you should have a solid understanding of how to configure Spring Security with JWT authentication. We've covered everything from adding dependencies to testing endpoints, and you've built a robust foundation for securing your Spring Boot applications.

Remember, security is an ongoing process. Stay curious, keep learning, and always be proactive in protecting your applications. Happy coding, and may your endpoints ever be secure!