Spring Boot JWT Authentication

JWT (JSON Web Token) is a stateless authentication method. After a user logs in, the server issues a token. The user sends this token with every request. The server verifies the token without checking a database — making it fast and scalable.

Session vs JWT Comparison

Session-based (stateful):            JWT-based (stateless):
──────────────────────────────────   ──────────────────────────────────────
Login → server saves session ID      Login → server creates JWT token
Client stores session ID in cookie   Client stores token (header/storage)
Each request → server checks DB      Each request → server verifies token
Server holds state                   No server-side state needed
Doesn't scale well                   Scales easily across multiple servers

JWT Token Structure

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGljZSIsInJvbGUiOiJVU0VSIiwiZXhwIjoxNzE1MDAwMDAwfQ.abc123

 ────────────────────  ──────────────────────────────────────────────  ──────
        Header                          Payload                       Signature

Header (base64):             Payload (base64):
{                            {
  "alg": "HS256",              "sub": "alice",
  "typ": "JWT"                 "role": "USER",
}                              "exp": 1715000000,
                               "iat": 1714996400
                             }

The signature verifies the token has not been tampered with. Only your server knows the secret key used to create it.

Add the JWT Library

<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>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
</dependency>

JWT Utility Service

@Component
public class JwtService {

    @Value("${jwt.secret}")
    private String secretKey;

    private static final long EXPIRATION_MS = 86400000; // 24 hours

    // Generate a token for a user
    public String generateToken(String username) {
        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS))
            .signWith(getSignKey(), SignatureAlgorithm.HS256)
            .compact();
    }

    // Extract username from token
    public String extractUsername(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(getSignKey())
            .build()
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }

    // Validate the token
    public boolean isTokenValid(String token, UserDetails user) {
        String username = extractUsername(token);
        return username.equals(user.getUsername()) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return Jwts.parserBuilder().setSigningKey(getSignKey()).build()
            .parseClaimsJws(token).getBody().getExpiration();
    }

    private Key getSignKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

Authentication Flow

STEP 1 — Login

  Client: POST /api/auth/login
          Body: { "email": "alice@test.com", "password": "pass123" }
               │
               ▼
  Server validates credentials
               │
               ▼
  Server generates JWT: eyJhbGci...
               │
               ▼
  Client receives: { "token": "eyJhbGci..." }

──────────────────────────────────────────────────────────────────────

STEP 2 — Subsequent Requests

  Client: GET /api/orders
          Header: Authorization: Bearer eyJhbGci...
               │
               ▼
  JwtAuthFilter extracts token from header
               │
               ▼
  JwtService.extractUsername(token) → "alice"
               │
               ▼
  Load UserDetails from DB
               │
               ▼
  JwtService.isTokenValid(token, userDetails) → true
               │
               ▼
  Set authentication in SecurityContext
               │
               ▼
  OrderController.getOrders() runs

JWT Filter

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired private JwtService jwtService;
    @Autowired private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {

        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = header.substring(7);            // Remove "Bearer "
        String username = jwtService.extractUsername(token);

        if (username != null &&
            SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails =
                userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValid(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Register the Filter in SecurityConfig

.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

Summary

  • JWT is stateless — the server stores no session data
  • A JWT token contains a header, payload, and signature
  • The server issues a token on login; the client sends it with every request
  • A custom filter reads the token from the Authorization header, validates it, and sets the security context
  • Store the secret key in environment variables — never in source code

Leave a Comment

Your email address will not be published. Required fields are marked *