Implémenter une authentification et autorisation robustes avec Spring Security et JWT
Dans la plupart des applications modernes, l’authentification et l’autorisation doivent rester fiables, sécurisées et maintenables. Une approche courante consiste à combiner Spring Security avec des JSON Web Tokens (JWT) afin de gérer l’accès côté serveur tout en permettant une consommation simple côté client (SPA, mobile, API).
Objectifs et principes de sécurité
Une implémentation robuste vise à :
- Authentifier un utilisateur via identifiants (ou un provider externe) et générer un JWT signé.
- Autoriser chaque requête via des règles basées sur le contenu du token et/ou les rôles.
- Réduire les risques liés aux erreurs courantes : gestion incorrecte des clés, token non expiré, validation insuffisante, ou absence de mécanismes anti-rejeu.
Architecture recommandée
Le flux typique est le suivant :
- Login : réception des identifiants, validation, création d’un JWT.
- Accès API : envoi du JWT dans l’en-tête
Authorization: Bearer <token>. - Filtrage : extraction du JWT, validation de la signature, vérification de l’expiration et chargement des autorités.
- Décision : Spring Security applique les règles d’accès (endpoints, rôles, expressions).
Configuration de Spring Security
Une configuration moderne s’appuie sur une chaîne de filtres et des règles explicites. L’objectif est de désactiver la session côté serveur (si l’API est stateless) et de brancher un filtre JWT.
Exemple de configuration (Spring Security 6 / 5.x)
csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login", "/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
]]>
Les règles ci-dessus assurent que seules les requêtes authentifiées peuvent accéder aux ressources protégées, et que /admin/** exige le rôle ADMIN.
Génération et validation de JWT
La sécurité d’un JWT repose principalement sur :
- La signature : le token doit être signé avec une clé secrète (HS256) ou une clé privée/public (RS256/ES256).
- L’expiration : un JWT doit avoir une durée de vie courte.
- La validation : signature, issuer, audience (optionnel), et expiration.
Service JWT (exemple simplifié)
claims, Duration ttl) {
Instant now = Instant.now();
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(ttl)))
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}
public Jws validateAndParse(String token) {
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build()
.parseClaimsJws(token);
}
}
]]>
Le service sépare la création du token et sa validation. Une validation centralisée limite les erreurs de configuration.
Filtre JWT : extraction et authentification
Le filtre JWT est chargé d’intercepter chaque requête, de récupérer le token et d’établir le SecurityContext.
Filtre JWT (exemple)
parsed = jwtService.validateAndParse(token);
Claims claims = parsed.getBody();
String username = claims.getSubject();
// Chargement des autorités (option A) depuis le système (DB)
UserDetails user = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(
user,
null,
user.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtException ex) {
// Token invalide / expiré : non-authentification, requête traitée selon règles
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}
]]>
Ce modèle peut soit s’appuyer sur les rôles présents dans le JWT (option B), soit recharger les rôles depuis une source de vérité (recommandé si la liste de rôles doit être réactive).
Autorisation : rôles, claims et expressions
Une autorisation robuste combine :
- Règles au niveau des endpoints (via
authorizeHttpRequests). - Règles au niveau des méthodes (via
@PreAuthorize). - Contrôle cohérent des rôles et/ou des claims.
Autorisation par annotation
getStats() {
return Map.of("status", "ok");
}
}
]]>
La méthode est protégée même si la route est exposée. Cette redondance renforce la sécurité.
Gestion des authentifications et endpoint de login
Le login typique implique un endpoint /auth/login qui vérifie les identifiants et renvoie le JWT.
Exemple de contrôleur de login
login(@RequestBody LoginRequest request) {
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password())
);
UserDetails user = (UserDetails) auth.getPrincipal();
Map claims = new HashMap<>();
claims.put("roles", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList());
String token = jwtService.generateToken(user.getUsername(), claims, Duration.ofMinutes(15));
return ResponseEntity.ok(Map.of(
"token", token,
"tokenType", "Bearer",
"expiresInSeconds", 900
));
}
}
]] code>
Les réponses de login doivent éviter de divulguer des informations excessives en cas d’échec.
Mesures anti-risque : anti-rejeu, refresh tokens et révocation
Un JWT seul ne suffit pas à couvrir tous les scénarios. Pour une robustesse accrue :
- Expiration courte : réduit l’impact d’un token compromis.
- Refresh token : permet de renouveler sans ré-authentification complète.
- Révocation : en cas de compromission, une stratégie (liste noire, rotation de refresh tokens, versionnement côté utilisateur) est utile.
- Anti-rejeu : un claim
jtiet un stockage côté serveur peuvent être employés si nécessaire. - Rotation des clés : planification de la rotation, support de plusieurs clés pendant la transition.
Bonnes pratiques de validation JWT
Une validation rigoureuse doit inclure :
- Signature : obligatoire.
- Expiration : rejet strict.
- Issuer : cohérence des tokens émis.
- Audience : optionnel mais recommandé pour réduire les usages croisés.
- Claims : contrôle de format (par ex. rôles attendus).
Observabilité et gestion d’erreurs
La sécurité s’améliore aussi via la visibilité :
- Journaliser les échecs de validation JWT avec prudence (sans inclure le token complet).
- Définir des réponses HTTP cohérentes : 401 pour non-authentifié, 403 pour non autorisé.
- Mettre en place des alertes sur des pics d’échecs d’authentification.
Conclusion
Une implémentation solide avec Spring Security et JWT repose sur une architecture claire : génération et validation centralisées, filtre JWT pour établir le SecurityContext, règles d’autorisation strictes au niveau route et/ou méthode, et mesures additionnelles (expiration courte, refresh tokens, révocation/rotation). Avec ces fondations, les applications conservent une sécurité cohérente dans le temps.
Checklist rapide : signature + expiration + validation des claims + endpoints protégés + règles de rôles + gestion des erreurs + plan de refresh/révocation.
À 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