DEV Community

Cover image for How I Built a Production-Ready JWT Auth Template with Java 21 and Spring Security 6
Mattia Forlani
Mattia Forlani

Posted on

How I Built a Production-Ready JWT Auth Template with Java 21 and Spring Security 6

How I Built a Production-Ready JWT Auth Template with Java 21 and Spring Security 6

Every Java project needs authentication. And every time, you spend days wiring up Spring Security, configuring JWT, setting up roles, handling token refresh... before you even write a single line of business logic.

I got tired of it. So I built a template I can reuse — and decided to share it.


What's Inside

  • Spring Boot 3.2 + Spring Security 6 stateless JWT auth
  • Java 21 features used throughout — Records, Sealed classes, Pattern matching
  • Role-based authorization — USER, ADMIN, MODERATOR out of the box
  • Access token + refresh token rotation
  • React + TypeScript frontend demo with password strength indicator
  • Docker + PostgreSQL — one command to run everything
  • Postman collection included

Why Java 21 Features Matter Here

This isn't just a Spring Boot project with Java 21 slapped on it. The modern Java features are used where they actually improve the code.

Records for DTOs

Instead of bloated POJOs with getters, setters, constructors and equals/hashCode:

public record LoginRequest(
    @NotBlank(message = "Username is required")
    String username,

    @NotBlank(message = "Password is required")
    String password
) {}
Enter fullscreen mode Exit fullscreen mode

Immutable, zero boilerplate, built-in equals, hashCode and toString. Spring validation works out of the box.

Sealed Classes for Exception Hierarchy

public sealed class AuthException extends RuntimeException
    permits AuthException.UserAlreadyExistsException,
            AuthException.InvalidTokenException,
            AuthException.UserNotFoundException,
            AuthException.RoleNotFoundException {

    public static final class UserAlreadyExistsException extends AuthException { ... }
    public static final class InvalidTokenException extends AuthException { ... }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The compiler knows every possible subtype. This means the switch expression in the exception handler is exhaustive — no unchecked cases.

Pattern Matching Switch in Exception Handler

var status = switch (ex) {
    case AuthException.UserAlreadyExistsException e -> HttpStatus.CONFLICT;
    case AuthException.InvalidTokenException e      -> HttpStatus.UNAUTHORIZED;
    case AuthException.RoleNotFoundException e      -> HttpStatus.INTERNAL_SERVER_ERROR;
    case AuthException.UserNotFoundException e      -> HttpStatus.NOT_FOUND;
    default                                         -> HttpStatus.BAD_REQUEST;
};
Enter fullscreen mode Exit fullscreen mode

Clean, readable, type-safe. No instanceof chains.

Optional Chain in JWT Filter

parseJwt(request)
    .filter(jwt -> SecurityContextHolder.getContext().getAuthentication() == null)
    .ifPresent(jwt -> {
        String username = jwtTokenProvider.extractUsername(jwt);
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        if (jwtTokenProvider.isTokenValid(jwt, userDetails)) {
            var authToken = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities()
            );
            authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
    });
Enter fullscreen mode Exit fullscreen mode

No nested null checks, no if/else chains.


Security Configuration

Spring Security 6 dropped WebSecurityConfigurerAdapter. The new approach uses beans directly:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(PUBLIC_ENDPOINTS).permitAll()
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .requestMatchers("/api/mod/**").hasAnyRole("ADMIN", "MODERATOR")
            .anyRequest().authenticated()
        )
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}
Enter fullscreen mode Exit fullscreen mode

Stateless, no sessions, JWT filter injected before Spring's default auth filter.


Password Validation

Both backend and frontend enforce strong passwords.

Backend@Pattern on the DTO rejects weak passwords even if someone bypasses the UI:

@Pattern(
    regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&_\\-#])[A-Za-z\\d@$!%*?&_\\-#]{8,}$",
    message = "Password must contain uppercase, lowercase, number and special character"
)
String password
Enter fullscreen mode Exit fullscreen mode

Frontend — real-time strength indicator with 5-segment bar and checklist that checks off rules as you type.


Docker Setup

services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: authdb
      POSTGRES_USER: authuser
      POSTGRES_PASSWORD: authpass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U authuser -d authdb"]
      interval: 5s
      timeout: 5s
      retries: 5
Enter fullscreen mode Exit fullscreen mode

The healthcheck on PostgreSQL ensures the app container only starts after the database is actually ready — not just running.


API Endpoints

Method Endpoint Auth Description
POST /api/auth/register Public Register new user
POST /api/auth/login Public Login, get tokens
POST /api/auth/refresh Public Refresh access token
POST /api/auth/logout Bearer Invalidate token
GET /api/me USER Current user info
GET /api/user/profile USER User profile
GET /api/mod/dashboard MOD+ Moderator area
GET /api/admin/users ADMIN Admin area

Get the Template

The full template with React frontend, Docker setup and Postman collection is available here:

👉 Spring Boot JWT Auth Template on Gumroad

Includes everything you need to clone it, run docker-compose up --build, and have a working auth system in minutes.


Tags

#java #springboot #webdev #security #jwt

Top comments (0)