Learn how to implement JWT authentication in Spring Boot with Java 21 using a complete, secure, end-to-end example with curl requests.
Introduction: Why JWT Authentication Matters 🔐
Imagine you log in to an application, refresh the page, and suddenly you’re logged out again.
Frustrating, right?
Modern applications—especially REST APIs—need a stateless, scalable, and secure way to identify users without storing session data on the server. This is exactly why JWT authentication in Spring Boot has become the industry standard.
JWT (JSON Web Token) is widely used in:
- Microservices
- Mobile apps
- Single Page Applications (SPAs)
- Cloud-native systems
In this blog, you’ll learn how to implement user authentication using JWT in Spring Boot, step by step, with fully working code, curl requests, and responses—all beginner-friendly and production-ready.
Core Concepts: JWT Explained Simply (ELI5) 🧒
Think of JWT Like a Movie Ticket 🎟️
- You buy a ticket → Login
- You get a ticket → JWT token
- You show the ticket at entry → API request
- The guard checks it → Spring Security filter
If the ticket is valid, you’re allowed in.
If not, access denied.
What Is JWT?
A JWT (JSON Web Token) is a compact, signed string that contains:
- Who the user is
- When the token expires
- Optional roles/claims
Example (shortened):
xxxxx.yyyyy.zzzzz
| Part | Purpose |
|---|---|
| Header | Token type & algorithm |
| Payload | User data (claims) |
| Signature | Ensures token wasn’t tampered |
Why JWT in Spring Boot?
Benefits
- Stateless (no server-side session)
- Scales easily
- Works well with REST APIs
- Secure when implemented correctly
Use Cases
- Login & authentication
- Protecting APIs
- Microservices security
End-to-End JWT Authentication Setup (Spring Boot + Java 21)
Tech Stack
- Java 21
- Spring Boot 3.x
- Spring Security 6
- JWT (jjwt)
1️⃣ Maven Dependencies
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JWT -->
<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>
2️⃣ JWT Utility Class (Token Generation & Validation)
@Component
public class JwtUtil {
private static final String SECRET_KEY =
"my-super-secure-secret-key-for-jwt-signing-12345";
private static final long EXPIRATION_MS = 15 * 60 * 1000;
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY.getBytes())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
Let’s break this JwtUtil class down line by line, in a beginner-friendly, real-world way.
Think of this class as a “JWT factory + inspector” 🏭🔍.
1️⃣ What Is JwtUtil in Simple Terms?
JwtUtil is a helper component that:
- Creates a JWT token when a user logs in
- Reads information from a JWT token when a request comes in
👉 It does two jobs only:
- Generate token
- Validate & extract username from token
2️⃣ Why Is It Annotated with @Component?
@Component
public class JwtUtil {
What this means:
- Spring creates one instance of this class
- You can inject it anywhere using constructor injection
👉 This makes JwtUtil reusable across controllers and filters.
3️⃣ Secret Key – The Lock & Key 🔐
private static final String SECRET_KEY =
"my-super-secure-secret-key-for-jwt-signing-12345";
What is this?
- This is the secret signing key
- Used to sign and verify JWT tokens
Why it’s important:
- If someone changes the token → signature breaks
- Token becomes invalid
👉 Think of it like:
A wax seal on an envelope.
If the seal is broken, you know the message was tampered with.
⚠️ Production Tip:
Never hardcode this key. Use environment variables or a secret manager.
4️⃣ Token Expiration Time ⏰
private static final long EXPIRATION_MS = 15 * 60 * 1000;
What this means:
- Token is valid for 15 minutes
- After that → token expires → user must log in again
Why expiration matters:
- Limits damage if a token is stolen
- Enforces better security
👉 Short-lived tokens = safer APIs
5️⃣ Generating the JWT Token 🏗️
public String generateToken(String username) {
This method is called after successful login.
Step-by-step breakdown
return Jwts.builder()
- Starts building a JWT
.setSubject(username)
- Stores the username inside the token
- This becomes the identity of the user
👉 Example inside JWT:
"sub": "user_123"
.setIssuedAt(new Date())
- Marks when the token was created
- Helps detect old or reused tokens
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
- Sets when the token will expire
- After this time → token is invalid
.signWith(
Keys.hmacShaKeyFor(SECRET_KEY.getBytes()),
SignatureAlgorithm.HS256
)
This is the most important line 🔐
- Uses HMAC SHA-256 (HS256) algorithm
- Signs the token using the secret key
- Prevents tampering
👉 If anyone edits the token payload:
- Signature validation fails
- Token is rejected
.compact();
- Final step
- Converts JWT into a string
Example output:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTY5...xyz
6️⃣ Extracting Username from JWT 🔍
public String extractUsername(String token) {
This method is used when:
- Client sends a request
- JWT is present in
Authorizationheader
Step-by-step breakdown
Jwts.parserBuilder()
- Creates a JWT parser
.setSigningKey(SECRET_KEY.getBytes())
- Uses the same secret key
- Verifies token signature
👉 If signature doesn’t match → exception thrown
.build()
.parseClaimsJws(token)
-
Validates:
- Signature
- Expiration
- Token structure
.getBody()
.getSubject();
- Reads the
sub(subject) claim - Returns the username
👉 Example return value:
user_123
7️⃣ What Happens If Token Is Invalid? ❌
If:
- Token is expired
- Token is modified
- Wrong secret key is used
👉 parseClaimsJws() throws an exception
👉 Request should be rejected (401 Unauthorized)
8️⃣ How This Class Fits in the Full Flow 🔄
Login Flow
User logs in
↓
generateToken(username)
↓
JWT sent to client
API Request Flow
Client sends JWT
↓
extractUsername(token)
↓
Spring Security sets authentication
↓
Request allowed
9️⃣ Real-World Analogy 🧠
| JWT Concept | Real Life |
|---|---|
| Token | Event pass |
| Secret key | Security stamp |
| Expiration | Pass expiry time |
| Signature | Anti-forgery seal |
10️⃣ Summary
-
JwtUtilcreates and validates JWT tokens - Uses HS256 symmetric encryption
- Stores username as subject
- Enforces token expiration
- Prevents tampering via signature
3️⃣ Authentication Controller (Login Endpoint)
@RestController
@RequestMapping("/auth")
public class AuthController {
private final JwtUtil jwtUtil;
public AuthController(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// Demo validation (replace with DB check in real apps)
if (!"password".equals(request.password())) {
return ResponseEntity.status(401).body("Invalid credentials");
}
String token = jwtUtil.generateToken(request.username());
return ResponseEntity.ok(new JwtResponse(token));
}
}
record LoginRequest(String username, String password) {}
record JwtResponse(String token) {}
🔹 Login Request (curl)
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "user_123",
"password": "password"
}'
🔹 Login Response
{
"token": "eyJhbGciOiJIUzI1NiJ9..."
}
4️⃣ JWT Filter (Validates Token on Every Request)
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@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);
String username = jwtUtil.extractUsername(token);
var auth = new UsernamePasswordAuthenticationToken(
username, null, List.of());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
Let’s walk through this JwtFilter step by step and understand exactly what it does, using simple language, real-world analogies, and Spring Security concepts.
Think of this class as a security guard at the entrance of your API 🛂.
1️⃣ What Is JwtFilter?
@Component
public class JwtFilter extends OncePerRequestFilter {
What this means:
-
@Component→ Spring manages this class (auto-created bean) -
OncePerRequestFilter→ Runs once for every HTTP request
👉 This filter:
- Intercepts every incoming request
- Checks for a JWT token
- Authenticates the user before the controller is called
2️⃣ Why Do We Need This Filter?
In JWT authentication:
- There is no server-side session
- Each request must prove identity again
👉 This filter does exactly that.
3️⃣ Dependency Injection: JwtUtil
private final JwtUtil jwtUtil;
public JwtFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
Why this is needed:
-
JwtUtil:- Validates JWT
- Extracts username
👉 The filter delegates JWT parsing to JwtUtil
👉 Keeps responsibilities clean (good design!)
4️⃣ Core Method: doFilterInternal
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
This method:
- Runs for every request
- Executes before your controller
- Decides whether the request is authenticated
5️⃣ Step 1: Read the Authorization Header
String authHeader = request.getHeader("Authorization");
Example header sent by client:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
👉 This is where JWT is carried.
6️⃣ Step 2: Validate Header Format
if (authHeader != null && authHeader.startsWith("Bearer ")) {
Why this check?
- Ensures header exists
- Ensures correct format (
Bearer <token>)
👉 Prevents errors and invalid requests.
7️⃣ Step 3: Extract the JWT Token
String token = authHeader.substring(7);
- Removes
"Bearer "(7 characters) - Extracts only the JWT token
Example:
Bearer abc.def.ghi
↓
abc.def.ghi
8️⃣ Step 4: Extract Username from Token 🔍
String username = jwtUtil.extractUsername(token);
What happens here:
- Token signature is verified
- Token expiration is checked
- Username (
subclaim) is extracted
👉 If token is:
- Expired ❌
- Tampered ❌
- Invalid ❌
➡️ An exception is thrown and authentication fails.
9️⃣ Step 5: Create Authentication Object 🔐
var auth = new UsernamePasswordAuthenticationToken(
username, null, List.of());
What is this?
- A Spring Security authentication object
- Represents an authenticated user
Parameters:
| Value | Meaning |
|---|---|
username |
Authenticated principal |
null |
No password needed (JWT already verified) |
List.of() |
No roles/authorities (yet) |
👉 At this point:
- Spring Security considers the user logged in
🔟 Step 6: Store Authentication in Security Context
SecurityContextHolder.getContext().setAuthentication(auth);
Why this is critical:
- Spring Security checks this context
-
Controllers use it to:
- Allow access
- Get user identity
👉 Without this line:
- User would be treated as unauthenticated
1️⃣1️⃣ Step 7: Continue the Request Chain
filterChain.doFilter(request, response);
What this does:
-
Passes control to:
- Next filter
- Eventually the controller
👉 Always call this or request will stop.
1️⃣2️⃣ What Happens If JWT Is Missing?
If:
Authorization header is absent
Then:
- Filter does nothing
- Request continues
- Spring Security may block access later (based on config)
1️⃣3️⃣ End-to-End Request Flow 🔄
Client sends request
↓
JwtFilter runs
↓
JWT extracted & validated
↓
Authentication set in context
↓
Controller executes
1️⃣4️⃣ Real-World Analogy 🧠
| JWT Filter Concept | Real Life |
|---|---|
| Filter | Security guard |
| JWT token | ID card |
| extractUsername | Checking name |
| SecurityContext | Visitor log |
| Controller | Office access |
1️⃣5️⃣ Important Notes & Improvements ⚠️
Current Implementation:
- ✅ Works for learning
- ❌ No exception handling
- ❌ No role support
- ❌ No token expiry handling here
Production Enhancements:
- Catch JWT exceptions
- Return 401 Unauthorized
- Add roles/authorities
- Support refresh tokens
✅ Summary
-
JwtFilterruns once per request - Extracts JWT from header
- Validates token
- Creates authentication object
- Sets user into Spring Security context
- Enables stateless authentication
5️⃣ Security Configuration (Spring Security 6)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
Let’s break down this SecurityConfig class slowly and clearly, and understand exactly how it controls security in your Spring Boot + JWT application.
Think of this class as the rulebook for your API security system 📘🔐.
1️⃣ What Is SecurityConfig?
@Configuration
@EnableWebSecurity
public class SecurityConfig {
What this means:
@Configuration
→ This class defines Spring beans (configuration rules)@EnableWebSecurity
→ Turns on Spring Security for the application
👉 Without this class, Spring Security would use default rules (which usually block everything).
2️⃣ Why Inject JwtFilter?
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
Purpose:
- Injects your custom JWT authentication filter
- Allows Spring Security to use JWT for authentication
👉 This connects your JWT logic to Spring Security’s filter chain.
3️⃣ The Heart of Security: SecurityFilterChain
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
What is SecurityFilterChain?
It is a pipeline of security filters that every request passes through.
Think of it like airport security checkpoints ✈️:
- ID check
- Baggage scan
- Final clearance
Each filter checks something.
4️⃣ Disabling CSRF Protection
.csrf(csrf -> csrf.disable())
Why disable CSRF?
- CSRF protection is mainly for session-based web apps
-
JWT APIs are:
- Stateless
- Use Authorization headers
👉 Disabling CSRF is standard practice for JWT-based REST APIs.
⚠️ Do not disable CSRF for browser-based session apps.
5️⃣ Authorization Rules (Who Can Access What)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
Rule 1: Public Endpoints
.requestMatchers("/auth/**").permitAll()
-
Allows anyone to access:
/auth/login-
/auth/register(if exists)
👉 No JWT required here.
Rule 2: Secure Everything Else
.anyRequest().authenticated()
-
Any other endpoint:
/api/**/orders/**/payments/**
👉 Requires:
- Valid JWT
- Successful authentication
6️⃣ Adding JWT Filter into the Chain 🧩
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
What this does:
- Inserts your
JwtFilter - Places it before Spring’s default authentication filter
Why before?
- JWT must be processed before Spring checks authentication
- Allows JWT-based identity to be set early
👉 Without this line:
- Spring would never see your JWT
- All secured requests would fail
7️⃣ Final Step: Build the Security Chain
.build();
- Finalizes the configuration
- Activates all security rules
8️⃣ End-to-End Request Flow 🔄
🔓 Public Endpoint (/auth/login)
Request → SecurityFilterChain → Allowed → Controller
🔐 Secured Endpoint (/api/hello)
Request
↓
JwtFilter extracts & validates JWT
↓
Authentication set in SecurityContext
↓
Spring Security checks authenticated()
↓
Controller executes
9️⃣ Real-World Analogy 🧠
| Spring Security Concept | Real Life |
|---|---|
| SecurityConfig | Office security policy |
| JwtFilter | ID verification desk |
| permitAll | Open lobby |
| authenticated | Restricted area |
| SecurityContext | Visitor badge |
🔟 What Happens If JWT Is Missing or Invalid?
- JwtFilter fails to authenticate
- SecurityContext remains empty
-
.authenticated()rule fails - Spring returns 401 Unauthorized
1️⃣1️⃣ Common Mistakes to Avoid ❌
- Forgetting to add JWT filter
- Placing JWT filter after authentication filter
- Disabling CSRF in browser apps
- Allowing too many endpoints publicly
1️⃣2️⃣ Summary
- Enables Spring Security
- Disables CSRF for stateless JWT APIs
- Allows
/auth/**without authentication - Protects all other endpoints
- Inserts custom JWT filter correctly
- Enforces stateless security
6️⃣ Protected API Endpoint
@RestController
@RequestMapping("/api")
public class SecureController {
@GetMapping("/hello")
public String hello(Authentication authentication) {
return "Hello " + authentication.getName() + ", you are authenticated!";
}
}
🔹 Secure API Call (curl)
curl http://localhost:8080/api/hello \
-H "Authorization: Bearer <JWT_TOKEN>"
🔹 Response
Hello user_123, you are authenticated!
Best Practices for JWT in Spring Boot ✅
- Always set token expiration
- Never store sensitive data in JWT
- Use HTTPS only
- Keep JWT secret strong & secure
- Validate token on every request
❌ Avoid:
- Long-lived tokens
- Putting passwords in JWT
- Skipping signature validation
Conclusion: JWT = Secure, Scalable Authentication 🚀
To recap:
- JWT enables stateless authentication
- Spring Boot + Spring Security make it clean and powerful
- Proper filters and validation are critical
- Works perfectly for modern APIs and microservices
If you’re learning JWT authentication in Spring Boot, this setup gives you a solid production-ready foundation.
Call to Action 💬
Have questions about JWT refresh tokens, roles & authorities, or securing microservices?
👇 Drop a comment below or ask for:
- JWT + Refresh Token implementation
- Role-based authorization
- Interview-ready JWT scenarios
Happy coding & secure your APIs the right way! 🔐
Top comments (0)