Chapter 12.3☕ 28 min read

Project 3: Secured Blog API (JWT + Roles + Comments)

This project builds a secure Blog backend. Only <code>ADMIN</code> users can create posts. Any <code>USER</code> can read posts and leave comments. This ties together Phase 7 (Spring Security 6, JWT, BCrypt, <code>@PreAuthorize</code>) and Phase 4 (JPA).

01The Concept: Role-Based Access Control

The Hyderabad Times Newspaper Analogy:

At a newspaper, there are two types of people:

  1. Journalists (ADMIN): They have the authority to write and publish articles.
  2. Readers (USER): They can read the articles and write comments in the feedback section.

If a Reader tries to access the "Publish Article" button, security stops them. We implement this using JWT and Spring Security's @PreAuthorize.

02Technical Explanation & Tied Concepts
  1. Spring Security 6 (Stateless): SecurityFilterChain configured with SessionCreationPolicy.STATELESS and our custom JwtAuthenticationFilter.
  2. @EnableMethodSecurity: Enables @PreAuthorize("hasRole('ADMIN')") on the controller methods.
  3. BCryptPasswordEncoder: Used during the /register endpoint to hash passwords before saving to the DB.
03Full Working Code (Core Security Flow)

Assume UserEntity, PostEntity, and Repositories exist.

1. Security Configuration (SecurityConfig.java)

package com.devinhyderabad;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableMethodSecurity
public class SecurityConfig {

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

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/posts/**").permitAll() // Reading posts is public
.anyRequest().authenticated()
)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

2. The Secured Controller (PostController.java)

package com.devinhyderabad;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

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

// 1. Public: Anyone can read posts
@GetMapping
public String getAllPosts() {
return "List of all blog posts.";
}

// 2. Secured: Only users with ROLE_ADMIN can create posts
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public String createPost(@RequestBody String postContent) {
return "Post created successfully by Admin: " + postContent;
}

// 3. Secured: Any authenticated user (USER or ADMIN) can comment
@PostMapping("/{id}/comments")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public String addComment(@PathVariable Long id) {
return "Comment added to post " + id;
}
}
04Why It Matters

The most critical part of this project is the JwtAuthenticationFilter. It must extract the JWT from the Authorization: Bearer <token> header, validate the signature, extract the username and roles, and set the UsernamePasswordAuthenticationToken inside the SecurityContextHolder. If the filter doesn't set the authorities correctly, @PreAuthorize will always return 403 Forbidden.

05Interview Note

Interview Note: The @EnableMethodSecurity annotation is the Spring Security 6 replacement for the now-deprecated @EnableGlobalMethodSecurity. It automatically enables @PreAuthorize, @PostAuthorize, and @Secured. Interviewers often ask: "Why does hasRole('ADMIN') require the role to be stored as ROLE_ADMIN in the database?" Because Spring Security automatically prefixes it.

Key Takeaways

  • ✅ @EnableMethodSecurity enables @PreAuthorize for method-level role-based access control
  • ✅ JwtAuthenticationFilter validates the token and sets the SecurityContext for each request
  • ✅ SessionCreationPolicy.STATELESS ensures no HTTP session — every request carries its own JWT
  • ✅ hasRole('ADMIN') looks for ROLE_ADMIN in the database — Spring adds the ROLE_ prefix automatically