Implementing Stateless Authentication with JSON Web Tokens in Spring Boot 2.x

Transitioning from Session-Based to Stateless Authentication

Traditional HTTP sessions maintain user state directly on the server. During authentication, the server creates a session object, persists it in memory or a shared cache, and returns a session identifier embedded in a client-side cookie. Every subsequent request automatically attaches this cookie, enabling the server to reconstruct the conversation context. While functional, this model introduces architectural constraints. In distributed systems, session replication or centralized caching increases latency and operational complexity. Cross-domain scenarios also face strict cookie-sharing limitations, making session-based approaches less suitable for modern microservices or mobile backends. JSON Web Tokens (JWT) provide a stateless alternative. Instead of relying on server-side storage, the token itself carries encoded user claims. The server validates the cryptographic signature to verify authenticity and integrity without tracking active sessions. This design eliminates state synchronization overhead, simplifies horizontal scaling, and enables seamless cross-origin API comunication. JWT Structural Components

A JWT is composed of three base64url-encoded sections concatenated with periods: - Header: Defines the token type (JWT) and the hashing algorithm (e.g., HMAC SHA256).

  • Payload: Contains registered claims such as expiration time and issuer, alongside custom application-specific data like user identifiers.
  • Signature: Ensures the token originates from a trusted source and has not been tampered with during transit.

Secure transmission typically follows the OAuth 2.0 Bearer Token specification, where clients append the token to the Authorization header: ``` Authorization: Bearer <token_string>


Project Configuration &amp; Core Implementation
-----------------------------------------------

Integrate the JSON Web Token library into you're build configuration: ```
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

Define JWT parameters in application.yml: ``` app: jwt: secret-key: YourBase64EncodedSecretKeyMustBeLongEnough token-expiration-ms: 3600000 auth-header-name: X-Auth-Token


Create a dedicated service to manage token lifecycle operations: ```
@Component
public class JwtTokenService {

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

    @Value("${app.jwt.token-expiration-ms}")
    private long expirationMillis;

    @Value("${app.jwt.auth-header-name}")
    private String headerName;

    public String generateToken(String subject) {
        Date issuedAt = new Date();
        Date expiry = new Date(System.currentTimeMillis() + expirationMillis);

        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(issuedAt)
                .setExpiration(expiry)
                .signWith(SignatureAlgorithm.HS512, secretKey)
                .compact();
    }

    public Claims extractClaims(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception ex) {
            return null;
        }
    }

    public boolean isTokenExpired(Date expiryDate) {
        return expiryDate.before(new Date());
    }

    public String getHeaderName() {
        return headerName;
    }
}

The service centralizes token generation, claim parsing, and expiration validation. Invalid tokens return null, allowing downstream validation layers to reject them explicitly. Request Interception & Validation

Enforce authentication by implementing a servlet filter equivalent within Spring MVC. An interceptor processes incoming requests before reaching controller logic: ``` @Component public class JwtAuthenticationInterceptor implements HandlerInterceptor {

private final JwtTokenService jwtService;

public JwtAuthenticationInterceptor(JwtTokenService jwtService) {
    this.jwtService = jwtService;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String path = request.getRequestURI();

    // Permit unauthenticated access for registration and login routes
    if (path.startsWith("/api/auth/login") || path.startsWith("/api/auth/register")) {
        return true;
    }

    String token = request.getHeader(jwtService.getHeaderName());
    if (token == null || token.isBlank()) {
        token = request.getParameter(jwtService.getHeaderName());
    }

    if (token == null || token.isBlank()) {
        throw new IllegalArgumentException("Missing authentication credential");
    }

    Claims claims = jwtService.extractClaims(token);
    if (claims == null || jwtService.isTokenExpired(claims.getExpiration())) {
        throw new IllegalArgumentException("Credential expired or malformed");
    }

    // Attach resolved user identity to the request context
    request.setAttribute("principal", claims.getSubject());
    return true;
}

}


Register the interceptor globally: ```
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final JwtAuthenticationInterceptor jwtInterceptor;

    public WebMvcConfig(JwtAuthenticationInterceptor jwtInterceptor) {
        this.jwtInterceptor = jwtInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/**");
    }
}

Controller Implementation

Demonstrate token issuance and protected resource access: ``` @RestController @RequestMapping("/api") public class SecureController {

private final JwtTokenService tokenService;

public SecureController(JwtTokenService tokenService) {
    this.tokenService = tokenService;
}

@PostMapping("/auth/login")
public ResponseEntity<map string="">> login(@RequestBody Map<string string=""> payload) {
    String username = payload.get("username");
    String password = payload.get("password");

    // Replace with actual repository authentication lookup
    if (!"demo_user".equals(username) || !"secure_pass".equals(password)) {
        return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
    }

    String accessToken = tokenService.generateToken(username);
    Map<string string=""> response = new HashMap<>();
    response.put("access_token", accessToken);
    response.put("issued_at", Instant.now().toString());

    return ResponseEntity.ok(response);
}

@GetMapping("/dashboard")
public ResponseEntity<map object="">> getDashboardData(HttpServletRequest request) {
    String authenticatedSubject = (String) request.getAttribute("principal");
    Map<string object=""> data = new HashMap<>();
    data.put("userId", authenticatedSubject);
    data.put("status", "active");
    data.put("theme", "dark");

    return ResponseEntity.ok(data);
}

}

Tags: jsonwebtoken spring-boot jwt-token stateless-authentication java-servlet-interceptor

Posted on Mon, 29 Jun 2026 17:00:30 +0000 by shashiku