Appearance
Spring Boot Security (Part 6) | JWT Authentication Implementation
The Wristband at a Music Festival Analogy
Imagine attending a three day outdoor music festival with fifty different stage tents, food stalls, and VIP lounges. At the main entrance gate on Friday morning, you present your ticket and government photo ID. The security staff verifies your ticket, cuts off your paper stub, and fastens a tamper proof holographic vinyl wristband onto your wrist.
Inside the festival grounds, you do not pull out your driver's license or ticket receipt every time you enter a stage tent. You hold up your wrist. The security guards at Stage 1, Stage 2, and the VIP lounge simply inspect the holographic wristband. If the wristband is intact, they let you through in one second.
In Spring Boot, JWT Authentication Implementation is that festival wristband. When a user authenticates at /api/auth/login, Spring verifies their credentials and returns a signed JWT wristband. For every subsequent API call, the client attaches that wristband in the Authorization: Bearer <token> header. A specialized custom security filter intercepts the request, validates the cryptographic signature, extracts the user's identity and roles, and loads them into Spring Security's context without hitting the database on every hop.
This lecture covers adding JJWT dependencies, writing a production grade JwtUtils component, implementing a custom OncePerRequestFilter, configuring stateless SecurityFilterChain, and building the complete authentication controller.
Architecture of the Spring Boot JWT Filter Chain
[ Client Request with Header: "Authorization: Bearer eyJhbGciOi..." ]
|
v
[ JwtAuthenticationFilter ] (extends OncePerRequestFilter)
|
1. Extracts "Bearer <token>" from Authorization header
2. Validates token signature & expiration via JwtUtils
3. Extracts username and roles from claims
4. Loads UserDetails from UserDetailsService
5. Constructs UsernamePasswordAuthenticationToken
6. SecurityContextHolder.getContext().setAuthentication(authToken)
|
v
[ UsernamePasswordAuthenticationFilter ]
|
v
[ Controller Handler Method ]Step 1: Add Dependencies in pom.xml
We use the industry standard JJWT (Java JSON Web Token) library by io.jsonwebtoken:
xml
<dependencies>
<!-- Spring Security & Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JJWT API, Implementation and Jackson Serializer -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>Step 2: Create the JwtUtils Utility Class
This class is responsible for generating, parsing, and validating tokens:
java
package com.example.orderservice.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtils {
// Must be at least 256 bits (32 bytes) for `HMAC SHA256`
@Value("${app.jwt.secret:mySecretKeyForJwtSigningMustBeVeryLongAndSecure12345678}")
private String jwtSecret;
// Token validity duration in milliseconds (e.g. 1 hour = 3600000 ms)
@Value("${app.jwt.expiration-ms:3600000}")
private long jwtExpirationMs;
private Key getSigningKey() {
byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
// 1. Generate Token from UserDetails
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().toString());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 2. Extract Username (Subject)
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 3. Extract Expiration Date
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(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// 4. Validate Token against UserDetails
public boolean validateToken(String token, UserDetails userDetails) {
try {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
} catch (JwtException | IllegalArgumentException e) {
System.err.println("Invalid JWT Token: " + e.getMessage());
return false;
}
}
}Step 3: Create the JwtAuthenticationFilter
Extend OncePerRequestFilter to ensure the filter executes exactly once per incoming request:
java
package com.example.orderservice.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.core.userdetails.UserDetailsService;
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 JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwtToken;
final String username;
// 1. Inspect Authorization header
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// 2. Extract raw token (after "Bearer ")
jwtToken = authHeader.substring(7);
try {
username = jwtUtils.extractUsername(jwtToken);
} catch (Exception e) {
filterChain.doFilter(request, response);
return;
}
// 3. If username is valid and no authentication exists in current SecurityContext
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 4. Validate token
if (jwtUtils.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 5. Authenticate user in SecurityContext
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
// 6. Continue filter chain
filterChain.doFilter(request, response);
}
}Step 4: Configure SecurityFilterChain
Wire your filter into Spring Security and configure stateless session management:
java
package com.example.orderservice.config;
import com.example.orderservice.security.JwtAuthenticationFilter;
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.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
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
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.authenticationProvider(authenticationProvider())
// Insert custom JWT filter before standard UsernamePasswordAuthenticationFilter
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}Step 5: The Authentication Controller
Create an endpoint where clients submit credentials to receive a signed JWT:
java
package com.example.orderservice.controller;
import com.example.orderservice.dto.AuthRequest;
import com.example.orderservice.dto.AuthResponse;
import com.example.orderservice.security.JwtUtils;
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.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest request) {
// 1. Authenticate credentials (throws BadCredentialsException if invalid)
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
// 2. Load user details
final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
// 3. Generate JWT
final String jwt = jwtUtils.generateToken(userDetails);
return ResponseEntity.ok(new AuthResponse(jwt));
}
}Interview Questions & Pitfalls
Q1: Why should JwtAuthenticationFilter extend OncePerRequestFilter instead of implementing Filter?
In standard servlet environments, internal request forwarding (such as error dispatches or view forwards) can cause standard filters to execute multiple times for a single incoming request. OncePerRequestFilter guarantees that the filter's authentication logic executes exactly once per request lifecycle, preventing duplicate database lookups.
Q2: Why must SessionCreationPolicy.STATELESS be configured when using JWT in Spring Security?
If SessionCreationPolicy.STATELESS is omitted, Spring Security defaults to creating an in memory HttpSession after the first request. The user would then be authenticated via session cookies rather than through the token, breaking statelessness and microservices clustering.
Q3: Where in the filter chain must JwtAuthenticationFilter be inserted?
It must be placed before UsernamePasswordAuthenticationFilter using .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class). This ensures the JWT token is extracted and the user is authenticated in the SecurityContext before downstream authorization filters evaluate route permissions.
Q4: How does Spring Security authenticate users during subsequent requests once a token is verified?
The custom filter creates a UsernamePasswordAuthenticationToken containing the user's UserDetails and GrantedAuthorities, and places it into the current thread's security context: SecurityContextHolder.getContext().setAuthentication(authToken). Downstream controllers and security checks read from this context directly.
Q5: What is the risk of using a weak, short secret key with HMAC SHA256?
HMAC SHA256 requires a secret key of at least 256 bits (32 characters). If you use a short dictionary word (like secret), an attacker who captures a single token can run offline brute force dictionary attacks on their GPU, crack the secret key in seconds, and begin forging valid administrator tokens.