JWT Token SpringBoot

A Complete Guide to JWT Authentication in Spring Boot (With File Breakdown)

πŸ” A Complete Guide to JWT Authentication in Spring Boot (With File Breakdown)

Welcome to this comprehensive tutorial on implementing JWT (JSON Web Token) authentication in a Spring Boot REST API project. By the end of this guide, you’ll have a fully functional, secure backend authentication system using JWTs, ready to integrate with any frontend like React, Angular, or even a mobile app.

In this tutorial, we’ll break down the entire structure of a secure JWT-based backend, file by file, along with the purpose and logic behind each file. You will write the code, and this post will be your blueprint.

🏒 Real-Life Analogy: JWT is Like an Employee Access Card

To better understand how JWT works, let’s relate it to something real-world:

πŸ“˜ Example:

An employee needs to enter a secure office building every day. Here’s how JWT mirrors this:

  1. πŸ” Login (Identity Check)
    Just like the employee shows ID on the first day, the user provides username and password.
  2. πŸ“‡ Access Card Issued (JWT Token)
    The company gives the employee an access card β€” just like your backend issues a JWT after successful login.
  3. πŸšͺ Entry at the Gate (Token on Every Request)
    Every day, the employee just flashes the card at the gate. Similarly, the user sends the token in the Authorization header.
  4. βœ… Quick Verification
    The gate scanner quickly checks if the card is valid and not expired. This is just like your backend validating the token.
  5. 🧾 No Re-Login Needed
    As long as the token is valid, no need to re-enter credentials β€” just like the employee doesn’t need to verify identity again.

This makes JWT extremely efficient β€” just like issuing reusable access cards that save time and provide security.

πŸ“ Project Structure O


πŸ“ Project Structure Overview

We’ll be organizing our Spring Boot project in a clean and modular way:

E:.
└───blog
    β”‚   SpringbootBlogApplication.java
    β”‚
    β”œβ”€β”€β”€config
    β”‚       DataSeeder.java
    β”‚       SecurityBeansConfig.java
    β”‚       SecurityConfigApi.java
    β”‚
    β”œβ”€β”€β”€controller
    β”‚   β”‚
    β”‚   β”œβ”€β”€β”€api
    β”‚   β”‚       AuthController.java
    β”‚   β”‚       CategoryApiController.java
    β”‚   β”‚       PostApiController.java
    β”‚   β”‚
    β”‚
    β”œβ”€β”€β”€dto
    β”‚       CategoryDTO.java
    β”‚       PostDTO.java
    β”‚
    β”œβ”€β”€β”€entity
    β”‚       Category.java
    β”‚       Comment.java
    β”‚       Post.java
    β”‚       User.java
    β”‚
    β”œβ”€β”€β”€repository
    β”‚       CategoryRepository.java
    β”‚       CommentRepository.java
    β”‚       PostRepository.java
    β”‚       UserRepository.java
    β”‚
    β”œβ”€β”€β”€security
    β”‚       CustomUserDetailsService.java
    β”‚       JwtAuthFilter.java
    β”‚       JwtUtil.java
    β”‚

 

Let’s now dive into each of these folders and list the files you will create inside them.


🧠 Why JWT for Authentication?

Before we start writing files, let’s understand why JWT is a great choice:

  • Stateless authentication: No need to store sessions in DB
  • Easy to share tokens between frontend and backend
  • Lightweight and compact
  • Built-in expiry mechanism for added security

πŸ—οΈ Step-by-Step Breakdown of Required Files

Here’s a breakdown of all the files you need to create. Each section explains what the file is for, and where it fits in the authentication pipeline.

βœ… 1. Main Application Class

  • File: BlogApplication.java
package com.acesoftech.blog;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootBlogApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootBlogApplication.class, args);
    }
}

  • Purpose: Bootstraps your Spring Boot app.

πŸ” 2. Security Configuration

a. SecurityConfigApi.java

package com.acesoftech.blog.config;

import com.acesoftech.blog.security.JwtAuthFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;

import java.io.IOException;

@Configuration
@Order(1)
public class SecurityConfigApi {

    @Autowired
    private JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        System.out.println("βœ… SecurityConfigApi loaded");

        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/posts", "/api/posts/**").authenticated()
                .requestMatchers("/api/categories", "/api/categories/**").authenticated()
                .anyRequest().authenticated()
            )
            .exceptionHandling(exception -> exception
                .authenticationEntryPoint(new AuthenticationEntryPoint() {
                    @Override
                    public void commence(HttpServletRequest request, HttpServletResponse response,
                                         AuthenticationException authException) throws IOException {
                        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
                        response.setContentType("application/json");
                        response.getWriter().write("{\"error\": \"πŸ”’ Unauthorized: Missing or invalid token.\"}");
                    }
                })
                .accessDeniedHandler(new AccessDeniedHandler() {
                    @Override
                    public void handle(HttpServletRequest request, HttpServletResponse response,
                                       AccessDeniedException accessDeniedException) throws IOException {
                        response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
                        response.setContentType("application/json");
                        response.getWriter().write("{\"error\": \"🚫 Forbidden: You don’t have permission to access this resource.\"}");
                    }
                })
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore((request, response, chain) -> {
                HttpServletRequest req = (HttpServletRequest) request;
                System.out.println("πŸ›‘οΈ Matched secured route: " + req.getRequestURI());
                chain.doFilter(request, response);
            }, UsernamePasswordAuthenticationFilter.class)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

  • Configures Spring Security to use JWT filter, allows certain endpoints (like login/register), and secures others.

b. SecurityBeansConfig.java (Optional but recommended)

package com.acesoftech.blog.config;

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityBeansConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // βœ… Use BCrypt hashing
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
  • Declares PasswordEncoder, AuthenticationManager, and other beans separately for reuse.

πŸ“¦ 3. Authentication Logic

a. AuthController.java

  • Handles /login, /register, and possibly /refresh-token.
package com.acesoftech.blog.controller.api;

import com.acesoftech.blog.security.JwtUtil;
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.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import com.acesoftech.blog.security.CustomUserDetailsService;

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

    @Autowired
    private AuthenticationManager authManager;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        authManager.authenticate(
            new UsernamePasswordAuthenticationToken(request.username(), request.password())
        );

        final UserDetails userDetails = userDetailsService.loadUserByUsername(request.username());
        final String token = jwtUtil.generateToken(userDetails);

        return ResponseEntity.ok(new AuthResponse(token));
    }

    record LoginRequest(String username, String password) {}
    record AuthResponse(String token) {}
}


 

b. JwtUtil.java

  • Utility class to create, validate, and extract information from JWT tokens.
package com.acesoftech.blog.security;

import io.jsonwebtoken.*;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

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 SECRET_KEY;

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

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

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

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

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

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        // Example: claims.put("role", userDetails.getAuthorities());
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hrs
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes())
                .compact();
    }

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

 

c. JwtAuthFilter.java

// Step 1: JwtAuthFilter.java
package com.acesoftech.blog.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private CustomUserDetailsService userDetailsService;

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

        String authHeader = request.getHeader("Authorization");
        String token = null;
        String username = null;

        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            token = authHeader.substring(7);
            username = jwtUtil.extractUsername(token);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtUtil.validateToken(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}
  • Intercepts each request, validates the token, sets user details into Spring Security context.

d. CustomUserDetailsService.java

package com.acesoftech.blog.security;

import com.acesoftech.blog.entity.User;
import com.acesoftech.blog.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.Collections;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

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

        return new org.springframework.security.core.userdetails.User(
                user.getEmail(), user.getPassword(), Collections.emptyList()
        );
    }
}

 

  • Implements UserDetailsService, fetches user from DB and converts it to Spring Security’s UserDetails object.

πŸ“‡ 4. DTO Layer

a.Β  Β PostDTO.java

  • Request body format for login.
package com.acesoftech.blog.dto;

import java.time.LocalDateTime;

public class PostDTO {

    private Long id;
    private String title;
    private String slug;
    private String content;
    private boolean featured;
    private boolean active;
    private String imageName;
    private LocalDateTime createdAt;
    private Long categoryId; // Reference to Category by ID

    // Constructors
    public PostDTO() {}

    public PostDTO(Long id, String title, String slug, String content, boolean featured,
                   boolean active, String imageName, LocalDateTime createdAt, Long categoryId) {
        this.id = id;
        this.title = title;
        this.slug = slug;
        this.content = content;
        this.featured = featured;
        this.active = active;
        this.imageName = imageName;
        this.createdAt = createdAt;
        this.categoryId = categoryId;
    }

    // Getters and Setters

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getSlug() {
        return slug;
    }

    public void setSlug(String slug) {
        this.slug = slug;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public boolean isFeatured() {
        return featured;
    }

    public void setFeatured(boolean featured) {
        this.featured = featured;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    public String getImageName() {
        return imageName;
    }

    public void setImageName(String imageName) {
        this.imageName = imageName;
    }

    public LocalDateTime getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }

    public Long getCategoryId() {
        return categoryId;
    }

    public void setCategoryId(Long categoryId) {
        this.categoryId = categoryId;
    }
}

b. CategoryDTO.java

  • Request body format for registration.
package com.acesoftech.blog.dto;

public class CategoryDTO {

    private Long id;
    private String name;
    private String slug;
    private String description;
    private Long parentId;
    private boolean active;

    // Constructors
    public CategoryDTO() {}

    public CategoryDTO(Long id, String name, String slug, String description, Long parentId, boolean active) {
        this.id = id;
        this.name = name;
        this.slug = slug;
        this.description = description;
        this.parentId = parentId;
        this.active = active;
    }

    // Getters and Setters

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSlug() {
        return slug;
    }

    public void setSlug(String slug) {
        this.slug = slug;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }
}

 


🧍 5. Entity and Repository Layer

a. User.java

  • Entity mapped to DB, containing username, email, password, role.
package com.acesoftech.blog.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {

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

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;

    private String role = "USER"; // USER or ADMIN

    // Getters and setters
    public Long getId() {
        return id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

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

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }
}

 

b. Cateory.java

package com.acesoftech.blog.entity;

import jakarta.persistence.*;
import java.util.List;

@Entity
public class Category {

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

    private String name;
    private String slug;
    private String description;
    private Long parentId;
    private boolean active;

    @OneToMany(mappedBy = "category")
    private List<Post> posts;

    // Getter and Setter for id
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    // Getter and Setter for name
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    // Getter and Setter for slug
    public String getSlug() {
        return slug;
    }

    public void setSlug(String slug) {
        this.slug = slug;
    }

    // Getter and Setter for description
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    // Getter and Setter for parentId
    public Long getParentId() {
        return parentId;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    // Getter and Setter for active
    public boolean isActive() {
        return active;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    // Getter and Setter for posts
    public List<Post> getPosts() {
        return posts;
    }

    public void setPosts(List<Post> posts) {
        this.posts = posts;
    }
}

 

a. UserRepository.java

  • Extends JpaRepository for CRUD on User entity.
package com.acesoftech.blog.repository;

import com.acesoftech.blog.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email); // βœ… This is correct
}

 

b. PostRepository.java

package com.acesoftech.blog.repository;

import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.entity.Post;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    Post findBySlug(String slug);

    List<Post> findByActiveTrueOrderByIdDesc();

    List<Post> findByCategoryAndActiveTrueOrderByIdDesc(Category category);

    List<Post> findTop3ByFeaturedTrueAndActiveTrueOrderByIdDesc();

    List<Post> findTop9ByActiveTrueOrderByIdDesc();
}

 

 

C. CategoryRepository.java

package com.acesoftech.blog.repository;

import com.acesoftech.blog.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CategoryRepository extends JpaRepository<Category, Long> {
    Category findBySlug(String slug);
}

 


πŸš€ 6. Seeder (Optional)

a. DataSeeder.java

package com.acesoftech.blog.config;

import com.acesoftech.blog.entity.User;
import com.acesoftech.blog.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * This class seeds an initial admin user when the application starts.
 */
@Component
public class DataSeeder implements CommandLineRunner {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Autowired
    public DataSeeder(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public void run(String... args) {
        String adminEmail = "admin@example.com";

        userRepository.findByEmail(adminEmail).ifPresentOrElse(
            user -> System.out.println("ℹ️ Admin user already exists: " + adminEmail),
            () -> {
                User admin = new User();
                admin.setEmail(adminEmail);
                admin.setPassword(passwordEncoder.encode("admin123")); // default password
                admin.setRole("ADMIN");
                userRepository.save(admin);

                System.out.println("βœ… Admin user created successfully.");
                System.out.println("πŸ“§ Email: " + adminEmail);
                System.out.println("πŸ”‘ Password: admin123");
            }
        );
    }
}

 

  • Pre-loads the DB with an admin user or test data.

🌐 7. Secured API Controller (Example)

a. PostApiController.java

 

package com.acesoftech.blog.controller.api;

import com.acesoftech.blog.dto.PostDTO;
import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.entity.Post;
import com.acesoftech.blog.repository.CategoryRepository;
import com.acesoftech.blog.repository.PostRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/posts")
public class PostApiController {

    @Autowired
    private PostRepository postRepository;

    @Autowired
    private CategoryRepository categoryRepository;

    @GetMapping
    public List<PostDTO> getAll() {
        return postRepository.findAll().stream().map(post -> {
            PostDTO dto = new PostDTO();
            dto.setId(post.getId());
            dto.setTitle(post.getTitle());
            dto.setSlug(post.getSlug());
            dto.setContent(post.getContent());
            dto.setActive(post.isActive());
            dto.setFeatured(post.isFeatured());
            dto.setCategoryId(post.getCategory().getId());
            dto.setImageName(post.getImageName());
            dto.setCreatedAt(post.getCreatedAt());
            return dto;
        }).collect(Collectors.toList());
    }

    @PostMapping(consumes = {"multipart/form-data"})
    public ResponseEntity<?> createPostWithImage(
            @RequestPart("post") String postJson,
            @RequestPart(value = "image", required = false) MultipartFile image) {

        ObjectMapper mapper = new ObjectMapper();
        PostDTO dto;
        try {
            dto = mapper.readValue(postJson, PostDTO.class);
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("Invalid JSON in 'post' part: " + e.getMessage());
        }

        Category category = categoryRepository.findById(dto.getCategoryId()).orElse(null);
        if (category == null) {
            return ResponseEntity.badRequest().body("Invalid category ID");
        }

        String imageName = null;
        if (image != null && !image.isEmpty()) {
            try {
                imageName = UUID.randomUUID() + "_" + image.getOriginalFilename();
                String uploadDir = System.getProperty("user.dir") + "/uploads/";
                Path uploadPath = Paths.get(uploadDir);
                Files.createDirectories(uploadPath);
                image.transferTo(uploadPath.resolve(imageName).toFile());
            } catch (Exception e) {
                return ResponseEntity.internalServerError().body("Image upload failed: " + e.getMessage());
            }
        }

        Post post = new Post();
        post.setTitle(dto.getTitle());
        post.setSlug(dto.getSlug());
        post.setContent(dto.getContent());
        post.setActive(dto.isActive());
        post.setFeatured(dto.isFeatured());
        post.setImageName(imageName);
        post.setCategory(category);
        post.setCreatedAt(LocalDateTime.now());

        Post saved = postRepository.save(post);

        dto.setId(saved.getId());
        dto.setImageName(imageName);
        dto.setCreatedAt(saved.getCreatedAt());

        return ResponseEntity.ok(dto);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable Long id) {
        postRepository.deleteById(id);
        return ResponseEntity.ok().build();
    }
}

 

  • Example controller to demonstrate how secured endpoints work with JWT.

a. CategoryApiController.java

package com.acesoftech.blog.controller.api;

import com.acesoftech.blog.dto.CategoryDTO;
import com.acesoftech.blog.entity.Category;
import com.acesoftech.blog.repository.CategoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/categories")
public class CategoryApiController {

    @Autowired
    private CategoryRepository categoryRepository;

    @GetMapping
    public List<CategoryDTO> getAll() {
        return categoryRepository.findAll().stream()
                .map(cat -> {
                    CategoryDTO dto = new CategoryDTO();
                    dto.setId(cat.getId());
                    dto.setName(cat.getName());
                    return dto;
                }).collect(Collectors.toList());
    }

    @PostMapping
    public CategoryDTO create(@RequestBody CategoryDTO dto) {
        Category category = new Category();
        category.setName(dto.getName());
        Category saved = categoryRepository.save(category);
        dto.setId(saved.getId());
        return dto;
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable Long id) {
        categoryRepository.deleteById(id);
        return ResponseEntity.ok().build();
    }
}

 



πŸ”„ JWT Authentication Workflow Summary

  1. User sends a login request with username/password
  2. AuthController verifies credentials
  3. JwtUtil generates a token
  4. Token is sent back in response
  5. On next requests, token is sent in Authorization: Bearer header
  6. JwtAuthFilter reads token, validates it
  7. CustomUserDetailsService loads the user and sets authentication context
  8. Secured endpoints are now accessible

πŸ›‘οΈ Key Security Considerations

  • Always encrypt passwords using BCryptPasswordEncoder
  • Set token expiration time in minutes/hours
  • Use HTTPS in production
  • Store token securely in frontend (HttpOnly cookie/localStorage)
  • Optional: add refresh tokens for long sessions

πŸ§ͺ Testing Endpoints

You can use Postman to test these endpoints:

  • POST /api/auth/login β†’ returns token
  • POST /api/auth/register β†’ creates a user
  • GET /api/posts β†’ protected route (needs token)

✨ What’s Next?

Once you complete the authentication setup:

  • Add role-based access (ADMIN, USER)
  • Add refresh tokens
  • Add logout support (optional with blacklist)
  • Secure sensitive endpoints
  • Hook it up to your React/Vue frontend

πŸ“š Final Thoughts

JWT authentication in Spring Boot may seem complex, but breaking it down file by file makes it manageable and scalable. With this blog as your blueprint, you’ll not only create secure APIs but also learn how to structure your code professionally.

You now have a solid plan β€” go ahead and implement each file with proper annotations, services, and logic. This architecture will serve you well in any production-grade project.

Let me know if you want me to generate any of the file codes or explain the token generation logic in detail. Happy coding! πŸš€

Leave a Reply

Your email address will not be published. Required fields are marked *