Spring Boot - Part 4 (Spring Security & JWT, Lombok, Actuators)

 

Spring Security + JWT

Spring Security adds a gatekeeper in front of your entire application. Every request passes through it before reaching your controllers. You define rules — which endpoints are open to everyone and which require the caller to prove their identity first.

JWT is how users prove their identity on every request. Instead of sending email and password on every request the user logs in once, gets a token, and sends that token with every subsequent request. The server reads the token and knows exactly who the user is.

Together they work like a concert venue:


Spring Security = the security system of the venue
JWT = the wristband you get at the entrance

You prove your identity once at the entrance (login), get a wristband (JWT token), and use that wristband to access different areas (protected endpoints) without proving your identity again.


# How JWT Authentication Works in Spring Boot

Step 1 — User sends email and password to POST /auth/login
Step 2 — Server verifies credentials against database
Step 3 — Valid → Server generates JWT token and sends it back
Step 4 — User stores the token
Step 5 — User sends token with every request :
Authorization: Bearer eyJhbGci...
Step 6 — JwtFilter reads and validates token on every request
Step 7 — Valid token → Spring Security knows who the user is
Step 8 — SecurityConfig rules decide if user can access endpoint
Step 9 — Request reaches controller or gets blocked with 403


The 6 Components of Spring Security & JWT 

1] JwtService (The Brain)

This is the brain of the entire JWT system. It is a utility class that knows how to do 3 things:

  • Generate a token — when a user logs in successfully, you call this to create a JWT token for them containing their email and expiry time

  • Extract email from token — when a request comes in with a token, you call this to find out who the user is

  • Validate a token — checks if the token is genuine and not expired

Why it matters: Every other component depends on this class. It's the foundation that makes JWT work in your application.

Technical details:

  • Uses a secret key to sign tokens (prevents tampering)

  • Tokens contain: subject (email), issued at, expiration time

  • Can add custom claims (roles, user ID, etc.)


2] JwtFilter (The Checkpoint Guard)

This is the first line of defense that intercepts every incoming request. Think of it as the security guard at the venue entrance who checks everyone's wristband. If you've used middleware in Node.js, you already understand JwtFilter! It's the exact same concept with different syntax.

What it does on every request:

  1. Looks for the wristband — extracts the Authorization header and checks if it starts with "Bearer "

  2. If no wristband — lets the request pass through to SecurityConfig (which may allow or deny based on public endpoints)

  3. If wristband exists — calls JwtService to:

    • Extract the email from the token

    • Validate if token is genuine and not expired

  4. If valid — creates an Authentication object and places it in Spring Security's SecurityContext (like telling the security system "this person is verified")

  5. If invalid — token is rejected and request never reaches controllers



Key Insight: Without this filter, Spring Security would never know about your JWT system. It's the bridge that connects your custom JWT logic to Spring Security's internal authentication system.

Why it extends OncePerRequestFilter: Ensures the filter executes exactly once per request, not multiple times if you have filter chains.


NOTE : The JWTFilter is just a simple middleware that we register inside SecurityConfig file instead of "app.use()" like in Nodejs. In Nodejs you build the filter chain from scratch with app.use(), Spring has 15+ built-in filters; you insert yours at specific positions.


3] UserDetailsService (The Identity Database Lookup)

This is Spring Security's way of finding who a user is. It's like the venue's customer database.

What it does:

  • Takes an email (extracted from JWT or login request)

  • Queries your database to find the user

  • Returns a UserDetails object containing:

    • Email/username

    • Password (stored encrypted)

    • Roles/permissions (e.g., ROLE_USER, ROLE_ADMIN)

Why it's needed: Spring Security doesn't know how your users are stored. This interface tells it how to load user information from YOUR database structure.


4] AuthenticationManager (The ID Checker)

This component handles the actual login process. It's the security guard at the entrance who checks your ID before giving you a wristband.

What it does only at login (/auth/login):

  • Receives email + password

  • Calls UserDetailsService to fetch the user

  • Compares provided password with stored password (using PasswordEncoder)

  • If match → authentication succeeds

  • If mismatch → throws exception (401 Unauthorized)

Why separate from JwtFilter:

  • JwtFilter handles requests after user has wristband

  • AuthenticationManager handles requests to get the wristband

Behind the scenes: AuthenticationManager is actually an interface. Spring Boot auto-configures a default implementation that uses DaoAuthenticationProvider, which in turn uses your UserDetailsService and PasswordEncoder.


5] SecurityConfig (The Venue Rulebook)

This is where you define who can go where. It's the overall security policy of your venue.

Two main things it does:

A) Defines endpoint access rules:


/auth/** → Open to everyone (entrance area)
/api/user/** → Requires USER role (general admission)
/api/admin/** → Requires ADMIN role (backstage pass)
/swagger-ui/** → Open (public documentation)

B) Configures security behavior:

  • Stateless sessions — tells Spring Security "don't create sessions, we're using JWT"

  • Disable CSRF — because JWT is stateless, CSRF protection isn't needed

  • Adds JwtFilter — inserts your custom filter before Spring Security's default authentication

Important: Even if JwtFilter sets authentication, SecurityConfig still checks if that authenticated user has permission for the specific endpoint. They work together:

  • JwtFilter says: "This user is John"

  • SecurityConfig says: "John is allowed to access /api/user/profile but NOT /api/admin/dashboard"

NOTE : We register request filters inside securityConfig but the order in which you add these filters or middlewares is different than nodeJs. In Nodejs the order in which you mention "app.use()" is order in which filters get applied, but Spring boot has 15+ built-in filters running, you insert yours at specific positions.


Request → [Security Filters] → [Your JwtFilter] → [More Filters] → Controller
Inserted here




// In your SecurityConfig class add filter like this :
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)

In the above example we put JwtFilter right BEFORE Spring's UsernamePasswordAuthenticationFilter, so  we validate JWT before we authenticate user.

Spring boot internally maintains a filter chain which is created at the start of application. When you add @EnableWebSecurity to your SecurityConfig class, Spring Security automatically:

  1. Creates the filter chain with all 15+ built-in filters

  2. Orders them in a specific sequence

  3. Registers them with your application


// Spring Security internal code (you don't write this)
public class SpringSecurityAutoConfiguration {
public void buildFilterChain() {
// Spring automatically creates and orders these filters:
List<Filter> filterChain = new ArrayList<>();
filterChain.add(new SecurityContextPersistenceFilter()); // #1
filterChain.add(new LogoutFilter()); // #2
filterChain.add(new UsernamePasswordAuthenticationFilter()); // #3
filterChain.add(new DefaultLoginPageGeneratingFilter()); // #4
filterChain.add(new BasicAuthenticationFilter()); // #5
filterChain.add(new RequestCacheAwareFilter()); // #6
filterChain.add(new SecurityContextHolderAwareRequestFilter()); // #7
filterChain.add(new AnonymousAuthenticationFilter()); // #8
filterChain.add(new SessionManagementFilter()); // #9
filterChain.add(new ExceptionTranslationFilter()); // #10
filterChain.add(new AuthorizationFilter()); // #11
// ... and more
return filterChain;
}
}

//-------------------------------------------------------------------------------

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
// You DON'T create the chain
// You just ADD your filter to Spring's existing chain
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}


    //----------------------------------------------------------------------------

┌─────────────────────────────────────────────────────────────────┐
│ SPRING BOOT STARTS │
├─────────────────────────────────────────────────────────────────┤
│ │
1. Spring scans for @EnableWebSecurity
│ ↓ │
2. Spring Security automatically creates filter chain │
│ with 15+ built-in filters (YOU DON'T WRITE THIS) │
│ ↓ │
│ 3. Spring sees your SecurityConfig with addFilterBefore() │
│ ↓ │
│ 4. Spring inserts YOUR JwtFilter into the existing chain │
│ ↓ │
│ 5. Final chain is ready → ALL requests flow through it │
│ │
└─────────────────────────────────────────────────────────────────┘


If you want to see what filters Spring creates, you can add this to your application.properties:


logging.level.org.springframework.security=DEBUG


Example] Below we define a simple securityConfig file.


package com.example.demo.config;

import com.example.demo.filter.JwtFilter;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}


The Builder Pattern is a design pattern where methods modify and return the same object, allowing you to chain method calls together in a fluent, readable sequence. Each method returns the same HttpSecurity object, allowing methods to be chained. This creates code that reads like a sentence describing the configuration.

After all methods are called and .build() executes, you get a SecurityFilterChain object that contains:

  • Request matcher — which URLs this chain applies to

  • Ordered filter list — all filters in correct sequence

  • Access rules — who can access what


http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth.requestMatchers("/auth/**").permitAll())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();



Application Startup Flow:

┌─────────────────────────────────────────────────────────────┐
1. Spring scans for @Configuration classes │
│ └── Finds SecurityConfig.java │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
2. Spring sees @EnableWebSecurity
│ └── Activates Spring Security │
│ └── Creates default filter chain │
│ └── Makes HttpSecurity available │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
3. Spring finds @Bean methods in SecurityConfig │
│ └── Calls filterChain(HttpSecurity) │
│ └── HttpSecurity is injected automatically │
│ └── Returns SecurityFilterChain │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
4. Spring registers SecurityFilterChain as a bean │
│ └── Overrides the default chain │
│ └── Now ALL requests follow YOUR rules │
└─────────────────────────────────────────────────────────────┘





@EnableWebSecurity

@EnableWebSecurity is Spring Security's master switch. It's the annotation that transforms your regular Spring Boot application into a secured application with authentication and authorization capabilities. It tells Spring: "Enable all security features for this application."

Spring Security automatically builds a chain of 15+ filters that every request must pass through. It imports all the necessary Spring Security configuration classes. It makes the HttpSecurity builder available so you can customize security rules.


6] PasswordEncoder (The Scrambler)

This is a utility that ensures passwords are never stored in plain text.

Two operations:

  • encode(password) — scrambles password before saving to database

  • matches(rawPassword, encodedPassword) — checks if login password matches stored scrambled version

Why BCrypt?

  • Built-in salt to prevent rainbow table attacks

  • Adaptive algorithm (can be made slower over time)

  • Industry standard for password hashing

Why needed: If your database gets hacked, plain text passwords are catastrophic. With BCrypt, attackers only get scrambled strings that are computationally infeasible to reverse.

The 3 Are Main Components

ComponentWhy It's MainWhy Others Are Support
JwtServiceCore JWT operationsEverything depends on it
JwtFilterConnects JWT to Spring SecurityWithout it, JWT is useless
SecurityConfigDefines all rulesWithout it, no security policy

The other 3 (UserDetailsService, AuthenticationManager, PasswordEncoder) are Spring Security's native components — they'd exist even without JWT. The main 3 are what you add to make JWT work with Spring Security.


NOTE : The "other 3" are for the login process only. Once a user has a JWT token, you only need JwtService and JwtFilter. If you're using custom authentication logic, you don't need UserDetailsService, PasswordEncoder, or AuthenticationManager.



╔═══════════════════════════════════════════════════════════════════╗
║ EVERY REQUEST FLOW ║
╚═══════════════════════════════════════════════════════════════════╝

Request arrives (with or without JWT)
JwtFilter ←──────────────────┐
↓ │
Looks for Bearer token │
↓ │
Calls JwtService to:
- Extract email from token │
- Validate token │
↓ │
If valid → Sets authentication │
in SecurityContext │
↓ │
SecurityConfig │
checks if authenticated user │
is allowed to access endpoint │
↓ │
Allowed? → Controller │
Denied?403 Forbidden │
Supporting components used:
- UserDetailsService (to load user) ───┘

╔═══════════════════════════════════════════════════════════════════╗
│ LOGIN FLOW ONLY ║
╚═══════════════════════════════════════════════════════════════════╝

POST /auth/login with email/password
AuthenticationManager verifies credentials
(uses UserDetailsService + PasswordEncoder)
JwtService generates token
Token returned to user


OncePerRequestFilter is an abstract base class provided by Spring that guarantees your filter executes exactly once per request, even if the request goes through multiple internal forwards, includes, or error pages. Every class that extends OncePerRequestFilter MUST override doFilterInternal().


┌─────────────────────────────────────────────────────────────────┐
│ OncePerRequestFilter │
│ (Abstract Class) │
├─────────────────────────────────────────────────────────────────┤
│ │
public final void doFilter(...) { │
// 1. Check if filter already ran for this request │
// 2. If not, mark as ran │
// 3. Call doFilterInternal() ← YOU IMPLEMENT THIS │
// 4. Continue chain │
│ } │
│ │
protected abstract void doFilterInternal(...); ← MUST OVERRIDE│
│ │
└─────────────────────────────────────────────────────────────────┘
│ extends
┌─────────────────────────────────────────────────────────────────┐
│ JwtFilter │
│ (Your Class) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ @Override
protected void doFilterInternal(...) { │
// YOUR CUSTOM JWT LOGIC HERE │
│ String token = extractToken(request); │
if (valid) { │
│ SecurityContextHolder.setAuthentication(...); │
│ } │
│ chain.doFilter(request, response); │
│ } │
│ │
└─────────────────────────────────────────────────────────────────┘



-----------------------------------------------------------------------------------------------------------------------------

Minimal Example


src/main/java/com/example/demo/
├── DemoApplication.java (Main class - not shown)
├── service/
│ └── JwtService.java ← @Service
├── filter/
│ └── JwtFilter.java ← @Component, extends OncePerRequestFilter
├── config/
│ └── SecurityConfig.java ← @Configuration, @EnableWebSecurity
├── controller/
│ ├── AuthController.java ← @RestController, @RequestMapping
│ └── DemoController.java ← @RestController
└── dto/
└── LoginRequest.java ← Plain POJO





// ============================================
// FILE: JwtService.java
// LOCATION: src/main/java/com/example/demo/service/JwtService.java
// ============================================
@Service // ← Tells Spring: "Create one instance of this class"
public class JwtService {
public String generateToken(String email) { ... }
public String extractEmail(String token) { ... }
public boolean isValid(String token) { ... }
}


// ============================================
// FILE: JwtFilter.java
// LOCATION: src/main/java/com/example/demo/filter/JwtFilter.java
// ============================================
@Component // ← Tells Spring: "Create and manage this class"
public class JwtFilter extends OncePerRequestFilter { // ← Ensures filter runs ONCE per request
private final JwtService jwtService; // ← Spring injects this automatically
public JwtFilter(JwtService jwtService) { // ← Constructor injection
this.jwtService = jwtService;
}
@Override // ← Override parent method
protected void doFilterInternal(
HttpServletRequest request, // ← Incoming request
HttpServletResponse response, // ← Outgoing response
FilterChain filterChain // ← Chain of remaining filters
) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtService.isValid(token)) {
String email = jwtService.extractEmail(token);
// Create authentication object
Authentication auth = new UsernamePasswordAuthenticationToken(email, null);
// Store in Spring Security context
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response); // ← Pass to next filter
}
}


// ============================================
// FILE: SecurityConfig.java
// LOCATION: src/main/java/com/example/demo/config/SecurityConfig.java
// ============================================
@Configuration // ← Tells Spring: "This class contains bean definitions"
@EnableWebSecurity // ← Tells Spring: "Enable Spring Security and create filter chain"
public class SecurityConfig {
private final JwtFilter jwtFilter; // ← Spring injects the JwtFilter bean
public SecurityConfig(JwtFilter jwtFilter) { // ← Constructor injection
this.jwtFilter = jwtFilter;
}
@Bean // ← Tells Spring: "Register the returned object as a bean"
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // ← Disable CSRF protection
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // ← No sessions
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // ← Public endpoints
.anyRequest().authenticated() // ← Everything else needs auth
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // ← Insert filter
.build();
}
}


// ============================================
// FILE: AuthController.java
// LOCATION: src/main/java/com/example/demo/controller/AuthController.java
// ============================================
@RestController // ← Tells Spring: "This is a REST controller"
@RequestMapping("/auth") // ← Base URL for all methods in this controller
public class AuthController {
private final JwtService jwtService; // ← Spring injects this
public AuthController(JwtService jwtService) { // ← Constructor injection
this.jwtService = jwtService;
}
@PostMapping("/login") // ← Maps POST /auth/login to this method
public String login(@RequestBody LoginRequest request) { // ← @RequestBody = get JSON from request body
// Verify credentials
// Generate token via JwtService
return jwtService.generateToken(request.getEmail());
}
}


// ============================================
// FILE: DemoController.java
// LOCATION: src/main/java/com/example/demo/controller/DemoController.java
// ============================================
@RestController // ← Tells Spring: "This is a REST controller"
public class DemoController {
@GetMapping("/hello") // ← Maps GET /hello to this method
public String hello() {
// Get authenticated user from SecurityContext
String email = SecurityContextHolder.getContext()
.getAuthentication()
.getName();
return "Hello " + email;
}
}


// ============================================
// FILE: LoginRequest.java
// LOCATION: src/main/java/com/example/demo/dto/LoginRequest.java
// ============================================
public class LoginRequest { // ← Plain Java class (no annotations needed)
private String email;
private String password;
// Getters and setters
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; }
}



-----------------------------------------------------------------------------------------------------------------------------

Example] Below we would be creating an example which uses JWT Service, JWT filter and SecurityConfig.


src/main/java/com/example/demo/
├── DemoApplication.java
├── service/
│ └── JwtService.java
├── filter/
│ └── JwtFilter.java
├── config/
│ └── SecurityConfig.java
└── controller/
├── AuthController.java
└── DemoController.java



// src/main/java/com/example/demo/DemoApplication.java

package com.example.demo;

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

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



// src/main/java/com/example/demo/service/JwtService.java

package com.example.demo.service;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;

import java.security.Key;
import java.util.Date;

@Service
public class JwtService {
private static final String SECRET = "mySuperSecretKeyForJWTGeneration12345678901234567890";
private static final long EXPIRATION = 86400000; // 24 hours in milliseconds
private Key getSigningKey() {
return Keys.hmacShaKeyFor(SECRET.getBytes());
}
public String generateToken(String email) {
return Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractEmail(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean isValid(String token) {
try {
extractEmail(token);
return true;
} catch (Exception e) {
return false;
}
}
}



// src/main/java/com/example/demo/filter/JwtFilter.java

package com.example.demo.filter;

import com.example.demo.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (jwtService.isValid(token)) {
String email = jwtService.extractEmail(token);
User user = User.withUsername(email)
.password("")
.authorities()
.build();
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}



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

package com.example.demo.config;

import com.example.demo.filter.JwtFilter;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}



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

package com.example.demo.controller;

import com.example.demo.service.JwtService;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/auth")
public class AuthController {
private final JwtService jwtService;
public AuthController(JwtService jwtService) {
this.jwtService = jwtService;
}
@PostMapping("/login")
public Map<String, String> login(@RequestBody Map<String, String> request) {
String email = request.get("email");
String password = request.get("password");
// Simple demo validation - accept any email with password "password"
if (email != null && "password".equals(password)) {
String token = jwtService.generateToken(email);
return Map.of("token", token, "email", email);
}
throw new RuntimeException("Invalid credentials");
}
}



// src/main/java/com/example/demo/controller/DemoController.java

package com.example.demo.controller;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class DemoController {
@GetMapping("/hello")
public Map<String, String> hello() {
String email = SecurityContextHolder.getContext()
.getAuthentication()
.getName();
return Map.of(
"message", "Hello " + email,
"status", "You are authenticated!"
);
}
@GetMapping("/public")
public Map<String, String> publicEndpoint() {
return Map.of("message", "This is public, no token needed!");
}
}


-----------------------------------------------------------------------------------------------------------------------------

Custom Request Filters

Custom filters are your own logic that you insert into Spring Security's filter chain. They allow you to intercept requests and responses at specific points in the request lifecycle.

Think of them as custom checkpoints in the security system — you decide what happens at each checkpoint.


HTTP Request
┌─────────────────────────────────────────────────────────────────┐
│ SPRING SECURITY FILTER CHAIN │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Built-in Filter 1] → [Built-in Filter 2] → [Built-in Filter 3]│
│ │ │ │ │
│ ▼ ▼ ▼ │
│ SecurityContext Logout UsernamePassword │
│ │
│ ↓ YOUR CUSTOM FILTERS ↓ │
│ │
│ [YOUR FILTER 1] → [YOUR FILTER 2] → [YOUR FILTER 3] │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ Logging Rate Limit JWT Validation │
│ │
│ ↓ │
│ [Built-in Filter 4] → [Built-in Filter 5] → ... │
│ │
└─────────────────────────────────────────────────────────────────┘
Your Controller

Below are 2 steps to create a custom request filter :

  • 1] Create a new Java class that extends OncePerRequestFilter and override doFilterInternal().
  • 2] In your SecurityConfig class, use one of the registration methods to insert your filter into Spring's filter chain. Without registration, Spring doesn't know WHERE in the chain to place your filter

Registration Methods

MethodSyntaxWhen to Use
addFilterBefore.addFilterBefore(yourFilter, ExistingFilter.class)Your filter must run BEFORE another filter
addFilterAfter.addFilterAfter(yourFilter, ExistingFilter.class)Your filter must run AFTER another filter
addFilterAt.addFilterAt(yourFilter, ExistingFilter.class)Replace an existing filter entirely




package com.example.demo.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class LoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// Before request - log incoming request
System.out.println("=== LOGGING FILTER ===");
System.out.println("Method: " + request.getMethod());
System.out.println("URI: " + request.getRequestURI());
System.out.println("IP: " + request.getRemoteAddr());
long startTime = System.currentTimeMillis();
// Continue the filter chain
filterChain.doFilter(request, response);
// After request - log response time
long duration = System.currentTimeMillis() - startTime;
System.out.println("Response Time: " + duration + "ms");
System.out.println("====================");
}
}


//----------------------------------------------------------------------------------------

package com.example.demo.config;

import com.example.demo.filter.JwtFilter;
import com.example.demo.filter.LoggingFilter;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtFilter jwtFilter;
private final LoggingFilter loggingFilter;
public SecurityConfig(JwtFilter jwtFilter, LoggingFilter loggingFilter) {
this.jwtFilter = jwtFilter;
this.loggingFilter = loggingFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
// Add logging filter FIRST (before everything)
.addFilterBefore(loggingFilter, SecurityContextPersistenceFilter.class)
// Add JWT filter before username/password auth
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}


----------------------------------------------------------------------------------------------------------------------------

Lombok

Lombok is a Java library that automatically generates boilerplate code at compile time using annotations. It saves you from writing repetitive code like getters, setters, constructors, toString, equals, hashCode, and builders.

Maven (pom.xml):


<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

Lombok is NOT specific to JPA. It's a general-purpose Java library that works with ANY Java class. JPA entities require many repetitive elements:

  • Getters/setters for each field

  • Constructors

  • equals() and hashCode()

  • toString() for debugging


// Without Lombok - 30+ lines
public class User {
private String name;
private int age;
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return "User{name=" + name + ", age=" + age + "}";
}
}

//-----------------------------------------------------------------------------------------

// With Lombok - 5 lines
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private int age;
}



┌─────────────────────────────────────────────────────────────────┐
│ HOW LOMBOK WORKS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: You write code with Lombok annotations │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Data │ │
│ │ public class User { │ │
│ │ private String name; │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 2: Java Compiler (javac) processes annotations │
│ └── Lombok annotation processor runs │
│ │ │
│ ▼ │
│ Step 3: Lombok generates code at COMPILE TIME │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ public class User { │ │
│ │ private String name; │ │
│ │ │ │
│ │ public String getName() { return name; } │ │
│ │ public void setName(String name) { this.name = name; }│ │
│ │ public String toString() { return "User(name=" + name + ")"; }│
│ │ // equals(), hashCode(), etc. │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Step 3: Your IDE sees the generated code │
│ └── You can call getName(), setName(), etc. │
│ │
└─────────────────────────────────────────────────────────────────┘

NOTE : Lombok generates code during compilation, not at runtime. The generated code is actually present in the compiled .class files.


----------------------------------------------------------------------------------------------------------------------------

Actuators

Actuators are production-ready endpoints that let you monitormanage, and inspect your running Spring Boot application. Think of them as:

  • Dashboard for your application
  • Health check system
  • Diagnostic tools for production
  • Control panel to see what's happening inside your app


┌─────────────────────────────────────────────────────────────────┐
│ YOUR SPRING BOOT APP │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ YOUR CODE │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │Controller│ │ Service │ │Repository│ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ACTUATOR ENDPOINTS │ │
│ │ │ │
│ │ /actuator/health → Reads health status │ │
│ │ /actuator/metrics → Reads JVM metrics │ │
│ │ /actuator/beans → Reads Spring context │ │
│ │ /actuator/env → Reads configuration │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ALL DATA IS EXPOSED VIA HTTP ENDPOINTS │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ EXTERNAL TOOLS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Browser → curl http://localhost:8080/actuator/health │
│ Prometheus → Scrapes metrics for monitoring │
│ Grafana → Creates dashboards │
│ Kubernetes → Uses /health for liveness/readiness probes │
│ │
└─────────────────────────────────────────────────────────────────┘




// Step 1: Add Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>


// Step 2: Configure (application.properties)
# Expose all endpoints
management.endpoints.web.exposure.include=*

# Show detailed health
management.endpoint.health.show-details=always

# Enable shutdown
management.endpoint.shutdown.enabled=true


// Step 3: Access
curl http://localhost:8080/actuator/health

Key Actuator Endpoints

EndpointWhat It ShowsExample Use
/actuator/healthApp health (UP/DOWN)Kubernetes liveness probe
/actuator/infoCustom app infoBuild version, app name
/actuator/metricsJVM metricsMemory, CPU, threads
/actuator/beansAll Spring beansVerify beans are loaded
/actuator/mappingsAll URL endpointsAPI documentation
/actuator/envEnvironment propertiesCheck configuration
/actuator/loggersLogging configurationChange log level at runtime
/actuator/threaddumpThread informationDebug deadlocks
/actuator/heapdumpMemory dumpAnalyze memory leaks


What Actuator Does NOT Give You

Not IncludedAlternative
Visual dashboardsSpring Boot Admin, Grafana
Historical dataPrometheus + Grafana
Alerts/notificationsPrometheus Alertmanager
Log aggregationELK Stack (Elasticsearch, Logstash, Kibana)

Actuator = Raw Data
Actuator + Tools = Complete Monitoring


Security Consideration

Actuator endpoints expose sensitive information!

Development (Open all)

java
.requestMatchers("/actuator/**").permitAll()

Production (Restrict)

java
.requestMatchers("/actuator/health").permitAll()  // Only health public
.requestMatchers("/actuator/**").hasRole("ADMIN") // Everything else needs ADMIN


-----------------------------------------------------------------------------------------------------------------------------





Comments

Popular posts from this blog

React Js + React-Redux (part-2)

React Js + CSS Styling + React Router (part-1)

ViteJS (Module Bundlers, Build Tools)