Retour aux articles

Implémenter une authentification et autorisation robustes avec Spring Security et JWT

Implémenter une authentification et autorisation robustes avec Spring Security et JWT | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular
```html

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 :

  1. Login : réception des identifiants, validation, création d’un JWT.
  2. Accès API : envoi du JWT dans l’en-tête Authorization: Bearer <token>.
  3. Filtrage : extraction du JWT, validation de la signature, vérification de l’expiration et chargement des autorités.
  4. 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 jti et 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