JWT Authentication with Spring Security
JWT (JSON Web Token) is the standard for stateless REST API authentication. No sessions, no server-side state — the token carries everything.
How JWT Works
1. Client sends POST /api/auth/login { username, password }
2. Server validates credentials
3. Server generates JWT → returns to client
4. Client stores JWT (localStorage, cookie)
5. Client sends JWT in header: Authorization: Bearer <token>
6. Server validates JWT on every request (no database lookup)
JWT Structure
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsInJvbGVzIjpbIkFETUlOIl0sImV4cCI6MTcxNDQ0MDAwMH0.signature
│ │ │
└── Header (algo) └── Payload (claims: sub, roles, exp) └── Signature
Dependencies
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
JWT Service
@Service
public class JwtService {
@Value("${app.security.jwt-secret}")
private String jwtSecret;
@Value("${app.security.jwt-expiration-ms}")
private long jwtExpirationMs;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = Map.of(
"roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList()
);
return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(getSigningKey())
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
private <T> T extractClaim(String token, Function<Claims, T> resolver) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return resolver.apply(claims);
}
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
}
}
JWT Authentication Filter
@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 {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
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());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Security Config
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Auth Controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authManager;
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
authManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password())
);
UserDetails userDetails = userDetailsService.loadUserByUsername(request.username());
String token = jwtService.generateToken(userDetails);
return ResponseEntity.ok(new AuthResponse(token));
}
}
public record LoginRequest(String username, String password) {}
public record AuthResponse(String token) {}
Testing
# Login
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' | jq -r '.token')
# Use token
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/books
Key Takeaways
- JWT = stateless auth — token carries identity and roles.
JwtAuthenticationFiltervalidates token on every request.SessionCreationPolicy.STATELESS— no server-side sessions.- Store the JWT secret securely — use environment variables.
- Set reasonable expiration (15 min–24 hours) with refresh tokens.
What’s Next?
Next: OAuth2 Login — sign in with Google, GitHub, and other providers.