Securing APIs with Spring Security and JWT: A Full-Stack Guide for Spring Boot & React

In our previous article on creating a CRUD application, we built a fully functional user management system (CRUD Operation). However, it has a critical vulnerability: all its API endpoints are public. Anyone can create, read, update, or delete users without any checks. It's time to lock it down.

So, this article will teach you how to implement robust, token-based authentication using Spring Security and JSON Web Tokens (JWT). We will secure our Spring Boot backend application and modify our React frontend to handle user registration, login, and authenticated requests.

By the end of this article, you'll understand how to protect your application and manage user sessions in a modern, stateless way, which is perfect for Single-Page Applications (SPAs) like React.

So, let's get started,

Prerequisites

In this tutorial, we will implement security in our previous CRUD application. You should have:

  • A working Spring Boot + React CRUD application.
  • Basic knowledge of Spring Boot and React.
  • JDK, Maven, Node.js, and MySQL are installed.

Part 1: Understanding the JWT Authentication Flow

Before we write any code, let's understand how JWT authentication works. Unlike traditional session-based authentication, JWT is stateless. This means the server doesn't need to store session information, which makes our application highly scalable.

Here's the flow:

  1. Login: The user sends their username and password to the backend application.
  2. Validation: The server validates the credentials with our database's stored credentials.
  3. Token Generation: If valid, the server generates a signed JWT token. This token contains user information (like username and roles) and an expiration date.
  4. Token to Client: The server sends this JWT back to the client.
  5. Token Storage: The React app stores the JWT, typically in localStorage, which can be used further.
  6. Authenticated Requests: For every subsequent request to a protected API endpoint, the client includes the JWT in the Authorization header as a Bearer Token, which is used as an authentic key.
  7. Server Verification: A security filter on the server intercepts the request, validates the JWT's signature and expiration, and if the key is valid, then processes the request; otherwise, it gives an error 401 unauthorized access.


Part 2: Enhancing the Spring Boot Backend for Security

Let's start by adding the necessary security layers to our backend application. Follow the necessary steps for your application.

Step 1: Add New Dependencies

Open your pom.xml and add the dependencies for Spring Security and JWT. We'll use the popular jjwt library. This is the following dependency that we have to add in our pom.xml, you should have to use the latest depency in your application. You can easily find these dependencies from the Maven Repository

  
  		<dependency>
			<groupid>org.springframework.boot</groupid>
			<artifactid>spring-boot-starter-security</artifactid>
		</dependency>
		<dependency>
			<groupid>io.jsonwebtoken</groupid>
			<artifactid>jjwt-impl</artifactid>
			<version>0.12.6</version>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupid>io.jsonwebtoken</groupid>
			<artifactid>jjwt-jackson</artifactid>
			<version>0.12.6</version>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupid>io.jsonwebtoken</groupid>
			<artifactid>jjwt-api</artifactid>
			<version>0.12.6</version>
		</dependency>
     

Step 2: Configure Application Properties for JWT

Add a secret key and token expiration time to your application.properties, so that we will use it in our application codebase. And remember, never hardcode secrets in your code!

properties

# JWT Configuration
jwt.secret=your-super-secret-key-that-is-long-and-secure-enough
jwt.expiration.ms=86400000 # 24 hours

Step 3: Create JWT Utility Class

We will create a class that will handle generating, parsing, and validating JWT tokens. So, the code below does the task of generating the JWT token from the username and adds the expiration time to it. And also, we will create a function that will fetch the username from the token. And we will create the validateToken function for validating the token, which will return either true or false based on the authenticity of the token.

src/main/java/com/example/crudbackend/security/JwtTokenProvider.java

Java

package com.example.crudbackend.security;

import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtTokenProvider {

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

    @Value("${jwt.expiration.ms}")
    private int jwtExpirationInMs;

    public String generateToken(Authentication authentication) {
        // In a real app, you'd get the user principal
        String username = authentication.getName();
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    public String getUsernameFromJWT(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException ex) {
            // Log the error
        }
        return false;
    }
}

Step 4: Create a Custom UserDetailsService

Spring Security needs to know how to load user data from our MySQL database. We'll implement the UserDetailsService interface.

So, basically, we have to tell Spring Security that we will consider the email as a username for authentication, and for that, we will implement the custom userDetailService.

First, add a password field to your User entity and update UserRepository to find a user by email.

User.java (add password field):

Java

private String password;

UserRepository.java (add new method):

Java

import java.util.Optional;
// ...
Optional findByEmail(String email);

Now, create the service.

src/main/java/com/example/crudbackend/service/CustomUserDetailsService.java

Java

package com.example.crudbackend.service;

import com.example.crudbackend.model.User;
import com.example.crudbackend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.ArrayList;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));

        return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), new ArrayList<>());
    }
}

Step 5: Create the JWT Authentication Filter

This filter will execute once per request. It will read the JWT from the header, validate it, and set the user's authentication in the security context.

src/main/java/com/example/crudbackend/security/JwtAuthenticationFilter.java

Java

package com.example.crudbackend.security;

import com.example.crudbackend.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;
    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                String username = tokenProvider.getUsernameFromJWT(jwt);
                UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

Step 6: Configure Spring Security

This is the main configuration file where we tie everything together. We'll define which endpoints are public (like login/register) and which are protected.

So, this is the main file, where we will configure the public or protected api, and other authentication things, CORS-related configuration, and many more.

src/main/java/com/example/crudbackend/config/SecurityConfig.java

Java

package com.example.crudbackend.config;

import com.example.crudbackend.security.JwtAuthenticationFilter;
import com.example.crudbackend.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() {
        return new JwtAuthenticationFilter();
    }

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
    }

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
            .and()
            .csrf().disable() // We disable CSRF because we are using JWT
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll() // Public endpoints
            .anyRequest().authenticated(); // All other endpoints require authentication

        // Add our custom JWT security filter
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

Step 7: Create an Authentication Controller

Finally, we need endpoints for user registration and login.

So, we will create a login and registration api where we take a request from the user and generate and validate the JWT by our predefined function that we will write already in the above files.

And when a user first registers at that time, we have to save the user's details to the database.

src/main/java/com/example/crudbackend/controller/AuthController.java

Java

package com.example.crudbackend.controller;

import com.example.crudbackend.model.User;
import com.example.crudbackend.payload.LoginRequest;
import com.example.crudbackend.payload.JwtAuthenticationResponse;
import com.example.crudbackend.repository.UserRepository;
import com.example.crudbackend.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

@CrossOrigin(origins = "http://localhost:3000")
@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    UserRepository userRepository;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    JwtTokenProvider tokenProvider;

    @PostMapping("/login")
    public ResponseEntity authenticateUser(@RequestBody LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword())
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);
        String jwt = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
    }

    @PostMapping("/register")
    public ResponseEntity registerUser(@RequestBody User signUpRequest) {
        if (userRepository.findByEmail(signUpRequest.getEmail()).isPresent()) {
            return ResponseEntity.badRequest().body("Email Address already in use!");
        }

        // Creating user's account
        User user = new User();
        user.setFirstName(signUpRequest.getFirstName());
        user.setLastName(signUpRequest.getLastName());
        user.setEmail(signUpRequest.getEmail());
        user.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));

        userRepository.save(user);
        return ResponseEntity.ok("User registered successfully");
    }
}

// Create simple payload classes for LoginRequest and JwtAuthenticationResponse
// e.g., LoginRequest.java with String email, String password
// e.g., JwtAuthenticationResponse.java with String accessToken

The backend is now secure! All requests to /api/v1/users/** will be rejected with a 401 Unauthorized error unless a valid JWT is provided.

Part 3: Updating the React Frontend

Now, our backend is ready now, let's make our React app aware of authentication. 

Step 1: Create Login/Register Components

Create new components for login and registration forms. For brevity, we'll focus on the logic. You can use simple Bootstrap forms.

Step 2: Update the API Service

Modify UserService.js to include login/register methods and set up an Axios interceptor. This interceptor will automatically add the JWT to the header of every API call. This is the most elegant way to handle tokens. So, when the user signs in, it sends the request to the backend, and from the backend, we get a token, and that token we will save to our local storage

src/services/AuthService.js (a new service file)

JavaScript

import axios from 'axios';

const API_URL = 'http://localhost:8080/api/auth/';

class AuthService {
    login(email, password) {
        return axios
            .post(API_URL + 'login', { email, password })
            .then(response => {
                if (response.data.accessToken) {
                    localStorage.setItem('user', JSON.stringify(response.data));
                }
                return response.data;
            });
    }

    logout() {
        localStorage.removeItem('user');
    }

    register(firstName, lastName, email, password) {
        return axios.post(API_URL + 'register', {
            firstName,
            lastName,
            email,
            password
        });
    }

    getCurrentUser() {
        return JSON.parse(localStorage.getItem('user'));
    }
}

export default new AuthService();

Now, let's create a central place for our authenticated Axios instance.

src/services/api.js

JavaScript

import axios from "axios";
import AuthService from "./AuthService";

const api = axios.create({
  baseURL: "http://localhost:8080/api/v1",
});

api.interceptors.request.use(
  (config) => {
    const user = AuthService.getCurrentUser();
    if (user && user.accessToken) {
      config.headers["Authorization"] = "Bearer " + user.accessToken;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

export default api;

Now, modify your UserService.js to use this new api instance instead of the base axios.

src/services/UserService.js (Updated)

JavaScript

import api from './api'; // Import the configured api instance

class UserService {
    getUsers() {
        return api.get('/users');
    }

    // ... other methods (createUser, updateUser, etc.) also use `api`
    createUser(user) {
        return api.post('/users', user);
    }
    // ...
}

export default new UserService();

Step 3: Manage Auth State in the App

In your main App.js, manage the authentication state. Show login/register links if not logged in, and show the user management UI and a logout button if logged in.

JavaScript

// In App.js
import React, { useState, useEffect } from 'react';
import AuthService from './services/AuthService';
// ... other imports

function App() {
    const [currentUser, setCurrentUser] = useState(undefined);

    useEffect(() => {
        const user = AuthService.getCurrentUser();
        if (user) {
            setCurrentUser(user);
        }
    }, []);

    const handleLogout = () => {
        AuthService.logout();
        setCurrentUser(undefined);
    };

    return (
        <div>
            <nav className="navbar navbar-expand navbar-dark bg-dark">
                <div className="navbar-nav mr-auto">
                    {/* Other nav items */}
                </div>

                {currentUser ? (
                    <div className="navbar-nav ml-auto">
                        <li className="nav-item">
                            <a href="/login" className="nav-link" onClick={handleLogout}>
                                Logout
                            </a>
                        </li>
                    </div>
                ) : (
                    <div className="navbar-nav ml-auto">
                        <li className="nav-item">
                            {/* Link to your login component */}
                        </li>
                    </div>
                )}
            </nav>

            <div className="container mt-3">
                {/* Use React Router to show components based on auth state and URL */}
                {currentUser ? (
                    // Show the UserManagementComponent
                ) : (
                    // Show Login/Register components
                )}
            </div>
        </div>
    );
}


Output:

Customize the CSS according to your requirements.

Registration page

Login Page


After Login




Conclusion

You have successfully transformed a simple CRUD application into a secure, enterprise-ready system. You learned how to:

  • Configure Spring Security from the ground up.
  • Implement stateless authentication using JSON Web Tokens (JWT).
  • Create public and protected API endpoints.
  • Hash passwords using BCrypt.
  • Handle login and registration on the React frontend.
  • Use Axios interceptors to seamlessly send the JWT with every authenticated request. 

Next Post Previous Post
No Comment
Add Comment
comment url