DEV Community

realNameHidden
realNameHidden

Posted on

How to Use JWT Authentication in Spring Boot (Java 21) — An End-to-End Beginner Guide

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
Enter fullscreen mode Exit fullscreen mode
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>
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Creates a JWT token when a user logs in
  2. 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 {
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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) {
Enter fullscreen mode Exit fullscreen mode

This method is called after successful login.


Step-by-step breakdown

return Jwts.builder()
Enter fullscreen mode Exit fullscreen mode
  • Starts building a JWT

.setSubject(username)
Enter fullscreen mode Exit fullscreen mode
  • Stores the username inside the token
  • This becomes the identity of the user

👉 Example inside JWT:

"sub": "user_123"
Enter fullscreen mode Exit fullscreen mode

.setIssuedAt(new Date())
Enter fullscreen mode Exit fullscreen mode
  • Marks when the token was created
  • Helps detect old or reused tokens

.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
Enter fullscreen mode Exit fullscreen mode
  • Sets when the token will expire
  • After this time → token is invalid

.signWith(
    Keys.hmacShaKeyFor(SECRET_KEY.getBytes()),
    SignatureAlgorithm.HS256
)
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
  • Final step
  • Converts JWT into a string

Example output:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTY5...xyz
Enter fullscreen mode Exit fullscreen mode

6️⃣ Extracting Username from JWT 🔍

public String extractUsername(String token) {
Enter fullscreen mode Exit fullscreen mode

This method is used when:

  • Client sends a request
  • JWT is present in Authorization header

Step-by-step breakdown

Jwts.parserBuilder()
Enter fullscreen mode Exit fullscreen mode
  • Creates a JWT parser

.setSigningKey(SECRET_KEY.getBytes())
Enter fullscreen mode Exit fullscreen mode
  • Uses the same secret key
  • Verifies token signature

👉 If signature doesn’t match → exception thrown


.build()
.parseClaimsJws(token)
Enter fullscreen mode Exit fullscreen mode
  • Validates:

    • Signature
    • Expiration
    • Token structure

.getBody()
.getSubject();
Enter fullscreen mode Exit fullscreen mode
  • Reads the sub (subject) claim
  • Returns the username

👉 Example return value:

user_123
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

API Request Flow

Client sends JWT
   ↓
extractUsername(token)
   ↓
Spring Security sets authentication
   ↓
Request allowed
Enter fullscreen mode Exit fullscreen mode

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

  • JwtUtil creates 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) {}
Enter fullscreen mode Exit fullscreen mode

🔹 Login Request (curl)

curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{
  "username": "user_123",
  "password": "password"
}'
Enter fullscreen mode Exit fullscreen mode

🔹 Login Response

{
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Example header sent by client:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Enter fullscreen mode Exit fullscreen mode

👉 This is where JWT is carried.


6️⃣ Step 2: Validate Header Format

if (authHeader != null && authHeader.startsWith("Bearer ")) {
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
  • Removes "Bearer " (7 characters)
  • Extracts only the JWT token

Example:

Bearer abc.def.ghi
↓
abc.def.ghi
Enter fullscreen mode Exit fullscreen mode

8️⃣ Step 4: Extract Username from Token 🔍

String username = jwtUtil.extractUsername(token);
Enter fullscreen mode Exit fullscreen mode

What happens here:

  • Token signature is verified
  • Token expiration is checked
  • Username (sub claim) 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());
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  • JwtFilter runs 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 {
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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()
)
Enter fullscreen mode Exit fullscreen mode

Rule 1: Public Endpoints

.requestMatchers("/auth/**").permitAll()
Enter fullscreen mode Exit fullscreen mode
  • Allows anyone to access:

    • /auth/login
    • /auth/register (if exists)

👉 No JWT required here.


Rule 2: Secure Everything Else

.anyRequest().authenticated()
Enter fullscreen mode Exit fullscreen mode
  • Any other endpoint:

    • /api/**
    • /orders/**
    • /payments/**

👉 Requires:

  • Valid JWT
  • Successful authentication

6️⃣ Adding JWT Filter into the Chain 🧩

.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
  • Finalizes the configuration
  • Activates all security rules

8️⃣ End-to-End Request Flow 🔄

🔓 Public Endpoint (/auth/login)

Request → SecurityFilterChain → Allowed → Controller
Enter fullscreen mode Exit fullscreen mode

🔐 Secured Endpoint (/api/hello)

Request
  ↓
JwtFilter extracts & validates JWT
  ↓
Authentication set in SecurityContext
  ↓
Spring Security checks authenticated()
  ↓
Controller executes
Enter fullscreen mode Exit fullscreen mode

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!";
    }
}
Enter fullscreen mode Exit fullscreen mode

🔹 Secure API Call (curl)

curl http://localhost:8080/api/hello \
-H "Authorization: Bearer <JWT_TOKEN>"
Enter fullscreen mode Exit fullscreen mode

🔹 Response

Hello user_123, you are authenticated!
Enter fullscreen mode Exit fullscreen mode

Best Practices for JWT in Spring Boot ✅

  1. Always set token expiration
  2. Never store sensitive data in JWT
  3. Use HTTPS only
  4. Keep JWT secret strong & secure
  5. 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)