# Spring Security 6 : Authentification JWT complète > Guide pratique pour implémenter une authentification JWT avec Spring Security 6. Configuration, génération de tokens, validation et bonnes pratiques. - Published: 2026-02-19 - Updated: 2026-03-31 - Author: SharpSkill - Tags: spring security, jwt, java, spring boot, authentification - Reading time: 15 min --- L'authentification JWT (JSON Web Token) représente la norme pour sécuriser les API REST modernes. Spring Security 6 introduit une nouvelle approche fonctionnelle de configuration qui simplifie l'implémentation tout en renforçant la sécurité. Ce guide couvre l'ensemble du processus, de la configuration initiale à la protection des endpoints. > **Prérequis** > > Ce tutoriel utilise Spring Boot 3.2+ et Spring Security 6.2+. Les concepts restent valables pour les versions ultérieures, avec des ajustements mineurs de syntaxe. ## Architecture JWT dans Spring Security L'authentification JWT repose sur un principe stateless : le serveur ne stocke aucune session. Chaque requête contient un token signé qui prouve l'identité de l'utilisateur. Cette architecture permet une scalabilité horizontale sans partage de session entre instances. Le flux d'authentification JWT se décompose en plusieurs étapes. L'utilisateur s'authentifie avec ses identifiants, reçoit un token JWT signé, puis inclut ce token dans chaque requête ultérieure. Le serveur valide la signature et extrait les informations utilisateur du token. ```java // JwtAuthenticationFlow.java // Représentation conceptuelle du flux d'authentification public class JwtAuthenticationFlow { // 1. Authentification initiale : POST /api/auth/login // → Vérification identifiants en base // → Génération du token JWT signé // → Retour du token au client // 2. Requêtes authentifiées : GET /api/protected // → Header: Authorization: Bearer // → Extraction et validation du token // → Création du SecurityContext // → Accès à la ressource protégée } ``` Cette approche élimine les problèmes de sticky sessions et simplifie le déploiement en environnement distribué. ## Configuration des dépendances Maven Le projet nécessite les dépendances Spring Security et une bibliothèque JWT. JJWT (Java JWT) offre une API fluide et bien maintenue pour la manipulation des tokens. ```xml org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-web io.jsonwebtoken jjwt-api 0.12.5 io.jsonwebtoken jjwt-impl 0.12.5 runtime io.jsonwebtoken jjwt-jackson 0.12.5 runtime org.springframework.boot spring-boot-starter-data-jpa ``` La séparation en trois modules JJWT (api, impl, jackson) suit le principe d'encapsulation : seule l'API est visible au compile-time, l'implémentation reste un détail runtime. ## Service de génération et validation JWT Le service JWT centralise toutes les opérations sur les tokens : génération, extraction des claims et validation. Une clé secrète sécurisée signe chaque token, garantissant son intégrité. ```java // JwtService.java @Service public class JwtService { // Clé secrète injectée depuis application.yml @Value("${app.jwt.secret}") private String secretKey; // Durée de validité du token (24 heures par défaut) @Value("${app.jwt.expiration:86400000}") private long jwtExpiration; // Génère un token JWT pour un utilisateur authentifié public String generateToken(UserDetails userDetails) { return generateToken(new HashMap<>(), userDetails); } // Génère un token avec des claims personnalisés public String generateToken(Map extraClaims, UserDetails userDetails) { return Jwts.builder() .claims(extraClaims) // Claims additionnels (rôles, permissions) .subject(userDetails.getUsername()) // Identifiant principal .issuedAt(new Date()) // Date de création .expiration(new Date(System.currentTimeMillis() + jwtExpiration)) .signWith(getSigningKey(), Jwts.SIG.HS256) // Signature HMAC-SHA256 .compact(); } // Extrait le username (subject) du token public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } // Extrait un claim spécifique via une fonction d'extraction public T extractClaim(String token, Function claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } // Valide le token : signature correcte et non expiré public boolean isTokenValid(String token, UserDetails userDetails) { final String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } // Vérifie si le token a expiré private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } // Extrait la date d'expiration private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } // Parse le token et extrait tous les claims private Claims extractAllClaims(String token) { return Jwts.parser() .verifyWith(getSigningKey()) // Vérifie la signature .build() .parseSignedClaims(token) // Parse le token signé .getPayload(); // Retourne les claims } // Génère la clé de signature depuis la clé secrète encodée en Base64 private SecretKey getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(keyBytes); } } ``` La clé secrète doit être suffisamment longue (256 bits minimum pour HS256) et stockée de manière sécurisée, jamais dans le code source. > **Sécurité de la clé secrète** > > Utilisez une variable d'environnement ou un gestionnaire de secrets pour la clé JWT. Une clé compromise permet de forger des tokens valides pour n'importe quel utilisateur. ## Configuration de l'entité User L'entité utilisateur implémente `UserDetails` de Spring Security, permettant une intégration directe avec le système d'authentification. ```java // User.java @Entity @Table(name = "users") public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String password; @Column(nullable = false) private String firstName; @Column(nullable = false) private String lastName; // Rôle stocké comme enum pour la sécurité de type @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; // Implémentation UserDetails : retourne les autorités de l'utilisateur @Override public Collection getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); } // Le username correspond à l'email dans cette implémentation @Override public String getUsername() { return email; } // Compte toujours actif (à adapter selon les besoins) @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } // Getters et setters omis pour la concision } ``` ```java // Role.java public enum Role { USER, // Utilisateur standard ADMIN // Administrateur avec privilèges étendus } ``` L'enum `Role` limite les valeurs possibles et facilite la vérification des autorisations dans les expressions SpEL. ## Filtre d'authentification JWT Le filtre JWT intercepte chaque requête pour extraire et valider le token. Il s'insère dans la chaîne de filtres Spring Security avant le filtre d'authentification standard. ```java // JwtAuthenticationFilter.java @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { // Récupère le header Authorization final String authHeader = request.getHeader("Authorization"); // Vérifie la présence du préfixe Bearer if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } // Extrait le token (sans le préfixe "Bearer ") final String jwt = authHeader.substring(7); try { // Extrait le username du token final String userEmail = jwtService.extractUsername(jwt); // Vérifie que l'utilisateur n'est pas déjà authentifié if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { // Charge les détails utilisateur depuis la base UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail); // Valide le token (signature + expiration + correspondance user) if (jwtService.isTokenValid(jwt, userDetails)) { // Crée l'objet d'authentification UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); // Ajoute les détails de la requête authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); // Définit l'authentification dans le contexte de sécurité SecurityContextHolder.getContext().setAuthentication(authToken); } } } catch (ExpiredJwtException e) { // Token expiré : l'utilisateur devra se réauthentifier response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Token expired"); return; } catch (JwtException e) { // Token invalide : signature incorrecte ou format malformé response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Invalid token"); return; } // Continue la chaîne de filtres filterChain.doFilter(request, response); } } ``` `OncePerRequestFilter` garantit que le filtre s'exécute une seule fois par requête, même en cas de forward ou include. ## Configuration Spring Security 6 Spring Security 6 utilise une approche fonctionnelle avec des lambdas pour configurer la chaîne de sécurité. Cette configuration définit les règles d'accès et intègre le filtre JWT. ```java // SecurityConfig.java @Configuration @EnableWebSecurity @EnableMethodSecurity // Active @PreAuthorize et @PostAuthorize @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // Désactive CSRF car l'API est stateless (pas de cookies de session) .csrf(csrf -> csrf.disable()) // Configure les règles d'autorisation .authorizeHttpRequests(auth -> auth // Endpoints publics : authentification et inscription .requestMatchers("/api/auth/**").permitAll() // Documentation API accessible sans authentification .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // Endpoints admin réservés aux administrateurs .requestMatchers("/api/admin/**").hasRole("ADMIN") // Toutes les autres requêtes nécessitent une authentification .anyRequest().authenticated() ) // Mode stateless : pas de session HTTP côté serveur .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) // Fournisseur d'authentification personnalisé .authenticationProvider(authenticationProvider) // Insère le filtre JWT avant le filtre d'authentification standard .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .build(); } } ``` ```java // ApplicationConfig.java @Configuration @RequiredArgsConstructor public class ApplicationConfig { private final UserRepository userRepository; // Service de chargement des utilisateurs pour Spring Security @Bean public UserDetailsService userDetailsService() { return username -> userRepository.findByEmail(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); } // Fournisseur d'authentification avec encoder de mot de passe @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService()); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } // Gestionnaire d'authentification exposé comme bean @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } // Encoder BCrypt pour le hashage sécurisé des mots de passe @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` La désactivation de CSRF est sécurisée pour les API stateless car l'authentification repose sur un header explicite, non sur un cookie automatiquement envoyé par le navigateur. ## Contrôleur d'authentification Le contrôleur expose les endpoints d'inscription et de connexion. Ces endpoints sont publics et retournent le token JWT après une authentification réussie. ```java // AuthenticationController.java @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthenticationController { private final AuthenticationService authService; // POST /api/auth/register - Inscription d'un nouvel utilisateur @PostMapping("/register") public ResponseEntity register( @Valid @RequestBody RegisterRequest request ) { return ResponseEntity.ok(authService.register(request)); } // POST /api/auth/login - Connexion utilisateur existant @PostMapping("/login") public ResponseEntity login( @Valid @RequestBody AuthenticationRequest request ) { return ResponseEntity.ok(authService.authenticate(request)); } // POST /api/auth/refresh - Rafraîchissement du token (optionnel) @PostMapping("/refresh") public ResponseEntity refresh( @RequestHeader("Authorization") String authHeader ) { return ResponseEntity.ok(authService.refreshToken(authHeader)); } } ``` ```java // AuthenticationRequest.java public record AuthenticationRequest( @NotBlank(message = "Email requis") @Email(message = "Format email invalide") String email, @NotBlank(message = "Mot de passe requis") String password ) {} ``` ```java // RegisterRequest.java public record RegisterRequest( @NotBlank(message = "Prénom requis") String firstName, @NotBlank(message = "Nom requis") String lastName, @NotBlank(message = "Email requis") @Email(message = "Format email invalide") String email, @NotBlank(message = "Mot de passe requis") @Size(min = 8, message = "Le mot de passe doit contenir au moins 8 caractères") String password ) {} ``` ```java // AuthenticationResponse.java public record AuthenticationResponse( String token, String type, long expiresIn ) { public AuthenticationResponse(String token, long expiresIn) { this(token, "Bearer", expiresIn); } } ``` Les records Java simplifient la définition des DTOs tout en garantissant l'immuabilité. ## Service d'authentification Le service orchestre la logique d'inscription et d'authentification, en déléguant au `JwtService` pour la génération des tokens. ```java // AuthenticationService.java @Service @RequiredArgsConstructor public class AuthenticationService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtService jwtService; private final AuthenticationManager authenticationManager; @Value("${app.jwt.expiration:86400000}") private long jwtExpiration; // Inscription d'un nouvel utilisateur @Transactional public AuthenticationResponse register(RegisterRequest request) { // Vérifie que l'email n'est pas déjà utilisé if (userRepository.existsByEmail(request.email())) { throw new EmailAlreadyExistsException("Email déjà enregistré"); } // Crée l'entité utilisateur avec mot de passe hashé User user = new User(); user.setFirstName(request.firstName()); user.setLastName(request.lastName()); user.setEmail(request.email()); user.setPassword(passwordEncoder.encode(request.password())); user.setRole(Role.USER); // Persiste l'utilisateur userRepository.save(user); // Génère et retourne le token JWT String jwtToken = jwtService.generateToken(user); return new AuthenticationResponse(jwtToken, jwtExpiration); } // Authentification d'un utilisateur existant public AuthenticationResponse authenticate(AuthenticationRequest request) { // Délègue la vérification au AuthenticationManager authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.email(), request.password() ) ); // Charge l'utilisateur (l'authentification a réussi) User user = userRepository.findByEmail(request.email()) .orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé")); // Génère le token avec claims additionnels Map claims = new HashMap<>(); claims.put("role", user.getRole().name()); claims.put("userId", user.getId()); String jwtToken = jwtService.generateToken(claims, user); return new AuthenticationResponse(jwtToken, jwtExpiration); } // Rafraîchissement du token (prolonge la session) public AuthenticationResponse refreshToken(String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { throw new InvalidTokenException("Token invalide"); } String oldToken = authHeader.substring(7); String userEmail = jwtService.extractUsername(oldToken); User user = userRepository.findByEmail(userEmail) .orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé")); // Génère un nouveau token String newToken = jwtService.generateToken(user); return new AuthenticationResponse(newToken, jwtExpiration); } } ``` L'`AuthenticationManager` centralise la vérification des identifiants, permettant de changer facilement de stratégie d'authentification. > **Refresh Token** > > Pour une sécurité renforcée, implémentez un système de refresh token avec une durée de vie plus longue, stocké en base de données et révocable. ## Protection des endpoints avec annotations La sécurité au niveau des méthodes permet un contrôle granulaire des autorisations directement dans le code métier. ```java // UserController.java @RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; // Accessible à tous les utilisateurs authentifiés @GetMapping("/me") public ResponseEntity getCurrentUser( @AuthenticationPrincipal User currentUser ) { return ResponseEntity.ok(UserDTO.from(currentUser)); } // Seul l'utilisateur concerné ou un admin peut modifier le profil @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") @PutMapping("/{id}") public ResponseEntity updateUser( @PathVariable Long id, @Valid @RequestBody UpdateUserRequest request ) { return ResponseEntity.ok(userService.updateUser(id, request)); } // Réservé aux administrateurs @PreAuthorize("hasRole('ADMIN')") @GetMapping public ResponseEntity> getAllUsers() { return ResponseEntity.ok(userService.findAll()); } // Suppression avec vérification post-exécution @PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{id}") public ResponseEntity deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.noContent().build(); } } ``` ```java // AdminController.java @RestController @RequestMapping("/api/admin") @PreAuthorize("hasRole('ADMIN')") // Toutes les méthodes requièrent ADMIN public class AdminController { private final AdminService adminService; @GetMapping("/dashboard") public ResponseEntity getDashboard() { return ResponseEntity.ok(adminService.getDashboardStats()); } @PostMapping("/users/{id}/role") public ResponseEntity changeUserRole( @PathVariable Long id, @RequestParam Role newRole ) { return ResponseEntity.ok(adminService.changeUserRole(id, newRole)); } } ``` L'annotation `@AuthenticationPrincipal` injecte directement l'utilisateur authentifié, évitant l'accès manuel au `SecurityContext`. ## Gestion des erreurs d'authentification Une gestion centralisée des erreurs assure des réponses cohérentes et informatives en cas d'échec d'authentification. ```java // SecurityExceptionHandler.java @RestControllerAdvice public class SecurityExceptionHandler { private static final Logger log = LoggerFactory.getLogger(SecurityExceptionHandler.class); // Erreur d'authentification (identifiants incorrects) @ExceptionHandler(BadCredentialsException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleBadCredentials(BadCredentialsException ex) { return new ErrorResponse( "INVALID_CREDENTIALS", "Email ou mot de passe incorrect", null ); } // Accès refusé (authentifié mais non autorisé) @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) public ErrorResponse handleAccessDenied(AccessDeniedException ex) { return new ErrorResponse( "ACCESS_DENIED", "Accès non autorisé à cette ressource", null ); } // Token JWT expiré @ExceptionHandler(ExpiredJwtException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleExpiredToken(ExpiredJwtException ex) { return new ErrorResponse( "TOKEN_EXPIRED", "Session expirée, veuillez vous reconnecter", null ); } // Token JWT invalide @ExceptionHandler(JwtException.class) @ResponseStatus(HttpStatus.UNAUTHORIZED) public ErrorResponse handleInvalidToken(JwtException ex) { log.warn("Invalid JWT token: {}", ex.getMessage()); return new ErrorResponse( "INVALID_TOKEN", "Token d'authentification invalide", null ); } // Email déjà utilisé lors de l'inscription @ExceptionHandler(EmailAlreadyExistsException.class) @ResponseStatus(HttpStatus.CONFLICT) public ErrorResponse handleEmailExists(EmailAlreadyExistsException ex) { return new ErrorResponse( "EMAIL_EXISTS", ex.getMessage(), null ); } } ``` ```java // ErrorResponse.java public record ErrorResponse( String code, String message, Map details ) {} ``` Les codes d'erreur standardisés facilitent le traitement côté client et le debugging. ## Configuration application.yml La configuration externalisée permet d'adapter les paramètres JWT selon l'environnement. ```yaml # application.yml app: jwt: # Clé secrète en Base64 (256 bits minimum pour HS256) # Générer avec : openssl rand -base64 32 secret: ${JWT_SECRET:votreCleSuperSecreteDe256BitsMinimum} # Durée de validité en millisecondes (24 heures) expiration: 86400000 spring: datasource: url: jdbc:postgresql://localhost:5432/springjwt username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:postgres} jpa: hibernate: ddl-auto: validate show-sql: false ``` ```yaml # application-dev.yml app: jwt: # Expiration courte pour le développement (1 heure) expiration: 3600000 spring: jpa: show-sql: true hibernate: ddl-auto: update logging: level: org.springframework.security: DEBUG ``` En production, la clé secrète doit provenir d'une variable d'environnement ou d'un service de gestion des secrets comme Vault. ## Tests d'intégration Les tests vérifient le comportement complet du système d'authentification, de l'inscription à l'accès aux ressources protégées. ```java // AuthenticationIntegrationTest.java @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = Replace.ANY) class AuthenticationIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Autowired private UserRepository userRepository; @BeforeEach void setUp() { userRepository.deleteAll(); } @Test void shouldRegisterAndAuthenticateUser() { // Inscription RegisterRequest registerRequest = new RegisterRequest( "John", "Doe", "john@example.com", "password123" ); ResponseEntity registerResponse = restTemplate .postForEntity("/api/auth/register", registerRequest, AuthenticationResponse.class); assertThat(registerResponse.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(registerResponse.getBody().token()).isNotBlank(); // Connexion avec les mêmes identifiants AuthenticationRequest loginRequest = new AuthenticationRequest( "john@example.com", "password123" ); ResponseEntity loginResponse = restTemplate .postForEntity("/api/auth/login", loginRequest, AuthenticationResponse.class); assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(loginResponse.getBody().token()).isNotBlank(); } @Test void shouldRejectInvalidCredentials() { AuthenticationRequest request = new AuthenticationRequest( "unknown@example.com", "wrongpassword" ); ResponseEntity response = restTemplate .postForEntity("/api/auth/login", request, ErrorResponse.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test void shouldAccessProtectedResourceWithValidToken() { // Inscription pour obtenir un token RegisterRequest registerRequest = new RegisterRequest( "Jane", "Doe", "jane@example.com", "password123" ); AuthenticationResponse authResponse = restTemplate .postForObject("/api/auth/register", registerRequest, AuthenticationResponse.class); // Accès à la ressource protégée avec le token HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(authResponse.token()); ResponseEntity response = restTemplate.exchange( "/api/users/me", HttpMethod.GET, new HttpEntity<>(headers), UserDTO.class ); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody().email()).isEqualTo("jane@example.com"); } @Test void shouldRejectAccessWithoutToken() { ResponseEntity response = restTemplate .getForEntity("/api/users/me", Void.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } } ``` Ces tests couvrent les scénarios principaux : inscription, connexion, accès autorisé et accès refusé. ## Conclusion L'implémentation d'une authentification JWT avec Spring Security 6 suit un pattern bien défini. Le service JWT gère la génération et validation des tokens, le filtre intercepte les requêtes pour établir le contexte de sécurité, et la configuration définit les règles d'accès. **Checklist de déploiement :** - ✅ Clé secrète de 256 bits minimum stockée en variable d'environnement - ✅ HTTPS obligatoire en production pour protéger les tokens en transit - ✅ Durée de validité des tokens adaptée au contexte (15 min à 24h) - ✅ Refresh token pour les sessions longues sans réauthentification - ✅ Gestion des erreurs standardisée avec codes métier - ✅ Logging des tentatives d'authentification échouées - ✅ Tests d'intégration couvrant les scénarios d'authentification - ✅ Rate limiting sur les endpoints d'authentification Cette architecture offre une base solide pour sécuriser les API REST, tout en restant extensible pour des besoins plus complexes comme l'authentification multi-facteurs ou l'intégration OAuth2. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/spring-boot/spring-security-6-jwt-authentification