6.3 JWT Stateless Authentication
← Back to Chapter 6: Spring Security & Authentication
JSON Web Tokens (JWT) enable stateless authentication in distributed systems. Because authentication state is encapsulated cryptographically inside the token itself, backend nodes require no shared Redis session state to verify requests.
1. Structure of a JWT
A JWT consists of three Base64URL-encoded parts separated by periods (.):
┌────────────┐ . ┌────────────┐ . ┌───────────────┐
│ HEADER │ │ PAYLOAD │ │ SIGNATURE │
└────────────┘ └────────────┘ └───────────────┘
- Header: Algorithm (
HS256,RS256) and token type (JWT). - Payload (Claims): Subject (
sub, e.g., username), expiration (exp), issued at (iat), roles/authorities. - Signature: HMAC or RSA digital signature calculated using a server-side private secret key to prevent tampering:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
2. The Custom JwtAuthenticationFilter
This filter intercepts every request, checks for the Bearer <token> header, validates the signature, and loads the user into SecurityContextHolder:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(7);
final String userEmail = jwtService.extractUsername(jwt);
// Authenticate only if user email is present and not already authenticated in this thread
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// Set authenticated context for downstream filters and controllers
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
3. Access Token vs Refresh Token Strategy
- Access Token: Short lifespan (e.g., 15 minutes). Transmitted on every HTTP request. If intercepted, exposure is limited.
- Refresh Token: Long lifespan (e.g., 7 days). Stored securely (in HTTP-only, secure cookies) or DB. Used exclusively to generate new access tokens without forcing re-login.