Implémentation avancée de Spring Security 6 avec JWT et Spring Boot 3.x pour API REST
Les applications modernes exposant des API REST nécessitent une authentification stateless, fiable et facile à faire évoluer. Spring Security 6, associé à Spring Boot 3.x et à des JWT (JSON Web Tokens), fournit une base robuste pour sécuriser les endpoints tout en conservant une architecture scalable.
Objectifs techniques
Cette approche vise à :
- Configurer une chaîne de sécurité Spring Security 6 adaptée aux API REST
- Mettre en place un JWT filter (authentification stateless)
- Gérer les rôles, les claims et l’autorisation fine
- Assurer une validation stricte : signature, expiration, audience/issuer
- Produire des réponses d’erreur cohérentes (401/403) et traçables
Prérequis
Les éléments suivants sont attendus :
- Java 17+ (recommandé avec Spring Boot 3.x)
- Spring Boot 3.x et Spring Security 6
- Bibliothèque JWT (ex.
io.jsonwebtokenounimbus-jose-jwt) - Une stratégie de stockage des secrets (env variables, vault, etc.)
Dépendances Maven (exemple)
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt-api
0.12.5
io.jsonwebtoken
jjwt-impl
0.12.5
runtime
io.jsonwebtoken
jjwt-jackson
0.12.5
runtime
]]>
Architecture recommandée
L’approche se structure généralement en quatre blocs :
- Provider / Service JWT : création et validation des tokens
- Filter JWT : extraction du bearer token et authentification
- Configuration Security (SecurityFilterChain) : règles d’accès et désactivation CSRF en REST
- Gestion d’erreurs : handlers d’authentification et d’accès
Modèle de claims JWT
Un design de claims efficace facilite l’autorisation. Par exemple :
- sub : identifiant utilisateur
- iss : émetteur
- aud : audience (optionnel mais recommandé)
- exp : expiration
- roles : liste des rôles
- scope : scopes optionnels (plus fin)
Service JWT : création et validation
Le service JWT encapsule la logique de signature et de parsing. Une validation stricte renforce la sécurité (signature, expiration, issuer, audience).
Exemple de service JWT
roles, long ttlMillis) {
long now = System.currentTimeMillis();
Date issuedAt = new Date(now);
Date expiration = new Date(now + ttlMillis);
return Jwts.builder()
.setSubject(subject)
.setIssuer(issuer)
.setAudience(audience)
.setIssuedAt(issuedAt)
.setExpiration(expiration)
.claim("roles", roles)
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}
public Jws validateAndParse(String token) {
JwtParser parser = Jwts.parserBuilder()
.setSigningKey(signingKey)
.requireIssuer(issuer)
.requireAudience(audience)
.build();
return parser.parseClaimsJws(token);
}
public boolean isExpired(String token) {
try {
validateAndParse(token);
return false;
} catch (ExpiredJwtException ex) {
return true;
}
}
}
]]>
Filter JWT : authentification stateless
Le filter inspecte l’en-tête Authorization, extrait le token de type Bearer, valide la signature et reconstruit le contexte de sécurité.
Exemple de JWT Authentication Filter
jws = jwtService.validateAndParse(token);
Claims claims = jws.getBody();
String subject = claims.getSubject();
@SuppressWarnings("unchecked")
List roles = claims.get("roles", List.class);
List authorities = roles.stream()
.map(r -> r.startsWith("ROLE_") ? r : "ROLE_" + r)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
var authentication = new UsernamePasswordAuthenticationToken(subject, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (io.jsonwebtoken.JwtException | IllegalArgumentException ex) {
// Token invalide, expiration, issuer/audience incorrect...
SecurityContextHolder.clearContext();
// La gestion finale 401 est déléguée au AuthenticationEntryPoint/Handler
}
filterChain.doFilter(request, response);
}
}
]]>
Configuration Spring Security 6 (SecurityFilterChain)
En Spring Security 6, la configuration passe typiquement par un bean SecurityFilterChain. Pour une API REST stateless, il est fréquent de désactiver CSRF et de configurer la gestion d’erreurs.
Exemple de configuration complète
response.sendError(org.springframework.http.HttpStatus.FORBIDDEN.value());
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers(new AntPathRequestMatcher("/api/auth/**")).permitAll()
.requestMatchers(HttpMethod.GET, "/api/public/**").permitAll()
// Example: stricte séparation
.requestMatchers(HttpMethod.POST, "/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
// Le reste requiert une authentification
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class)
.httpBasic(Customizer.withDefaults())
.formLogin(form -> form.disable());
return http.build();
}
}
]]>
Autorisation avancée : méthode et règles fines
Une pratique robuste consiste à combiner : des règles au niveau du router (SecurityFilterChain) et des règles au niveau méthode (annotations).
Exemple d’annotation method-level
Vérification de claims spécifiques
Lorsque l’autorisation dépend d’un attribut métier (ex. organisation, tenant, identifiant), l’approche avancée consiste à exposer des informations issues des claims dans le contexte puis à les exploiter via SpEL.
Exemple conceptuel : ajouter un claim tenant et vérifier l’accès selon le tenant cible. (La mise en place dépend du modèle d’application et du mapping des claims.)
Gestion des erreurs : réponse JSON cohérente
Pour une API REST, il est utile de renvoyer des erreurs structurées. Le handler peut produire une réponse JSON lors de : 401 Unauthorized (token invalide/absent) et 403 Forbidden (permissions insuffisantes).
Exemple d’entry point 401 JSON
Conseils de sécurité (checklist)
- Secrets robustes : longueur minimale correcte pour HMAC (éviter les clés faibles)
- Rotation : prévoir une rotation de clé et la gestion multi-clés si nécessaire
- Validation stricte : vérifier exp, iss, aud, signature
- Durée de vie : TTL court + mécanisme de renouvellement (refresh token) si requis
- Révocation : JWT stateless implique une stratégie explicite de révocation/blacklist si nécessaire
- Logs et traçabilité : éviter l’exposition des tokens dans les logs
- Least privilege : rôles/scope minimaux
Exemple de flux d’authentification
Étapes typiques
1. Le client appelle /api/auth/login.
2. Le serveur émet un JWT signé avec les claims nécessaires.
3. Le client inclut Authorization: Bearer <token> sur chaque requête.
4. Le filter valide le token et injecte le Authentication dans le contexte Spring.
5. Les règles d’accès autorisent ou refusent selon les rôles et/ou conditions méthode.
Conclusion
Une implémentation avancée de Spring Security 6 avec JWT pour Spring Boot 3.x repose sur une configuration stateless, un filter dédié à la validation JWT, une autorisation combinant règles de route et contrôles au niveau méthode, et une gestion d’erreurs adaptée aux API REST. Cette approche fournit un compromis solide entre sécurité, maintenabilité et évolutivité.
À propos de l'auteur
Laty Gueye Samba est développeur Full Stack basé à Dakar, Sénégal. Spécialiste des écosystèmes Java / Spring Boot et Angular.
Contact : latygueyesamba@gmail.com | Dakar, Sénégal