Dans l'écosystème du développement d'applications web modernes, la sécurité est une préoccupation primordiale. Les applications basées sur des API REST et des architectures de microservices nécessitent souvent une approche d'authentification différente de celle des applications monolithiques traditionnelles basées sur des sessions. C'est dans ce contexte que l'authentification sans état (stateless) avec JSON Web Tokens (JWT) et Spring Security 6, intégrée à Spring Boot 3.x, s'impose comme une solution robuste et scalable.
Cette approche permet de découpler la logique d'authentification de la gestion de session côté serveur, offrant une flexibilité accrue, notamment pour les applications mobiles, les SPAs (Single Page Applications) et les interactions inter-services. La maîtrise de ces techniques est cruciale pour construire des API sécurisées et performantes, capables de gérer une charge importante et de s'intégrer harmonieusement dans des écosystèmes distribués.
En tant que Développeur Full Stack Java Spring Boot + Angular basé à Dakar, Laty Gueye Samba observe une demande croissante pour des solutions de sécurité API robustes et performantes. La mise en œuvre de l'authentification sans état avec Spring Security JWT répond directement à ces besoins, permettant de développer des applications sécurisées pour divers secteurs, des systèmes de gestion hospitalière aux plateformes de gestion des risques, un domaine où la sécurité des données est non négociable.
JWT : Le pilier de l'authentification sans état
Le JSON Web Token (JWT) est une norme ouverte (RFC 7519) qui définit une manière compacte et auto-contenue de transmettre des informations en toute sécurité entre les parties sous forme d'objet JSON. Ces informations peuvent être vérifiées et fiables car elles sont signées numériquement. Un JWT se compose de trois parties, séparées par des points :
- En-tête (Header) : Contient le type de token (JWT) et l'algorithme de signature utilisé (HMAC SHA256 ou RSA).
- Charge utile (Payload) : Contient les revendications (claims). Ce sont des déclarations sur une entité (généralement l'utilisateur) et des données supplémentaires. Elles peuvent être enregistrées (comme
isspour l'émetteur,exppour l'expiration), publiques ou privées. - Signature (Signature) : Calculée en encodant l'en-tête et la charge utile en Base64Url, puis en appliquant l'algorithme spécifié dans l'en-tête avec une clé secrète. Cette signature permet de vérifier que le message n'a pas été altéré.
L'avantage majeur du JWT dans un contexte d'authentification est sa nature sans état. Une fois qu'un utilisateur est authentifié, un JWT est émis et envoyé au client. Lors des requêtes ultérieures, le client renvoie ce JWT dans l'en-tête d'autorisation. Le serveur peut alors valider le token de manière autonome (en vérifiant la signature et la date d'expiration) sans avoir besoin de consulter une base de données de sessions, ce qui améliore la scalabilité et la performance des API.
Configuration de Spring Security 6 pour l'authentification JWT
Avec Spring Boot 3.x et Spring Security 6, la configuration pour JWT est modernisée. L'objectif est d'intercepter les requêtes entrantes, de valider le JWT, et d'établir le contexte de sécurité de Spring si le token est valide. Voici les étapes clés :
1. Dépendances nécessaires
Il est nécessaire d'ajouter les dépendances Spring Security et JJWT (pour la manipulation des tokens) au fichier pom.xml ou build.gradle :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
2. Configuration de la chaîne de filtres de sécurité
Une classe de configuration étendant WebSecurityConfigurerAdapter est maintenant remplacée par une configuration via un Bean de type SecurityFilterChain. Il est crucial de désactiver la protection CSRF et la gestion des sessions pour une API sans état.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Active la sécurité au niveau des méthodes (@PreAuthorize)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter, AuthenticationProvider authenticationProvider) {
this.jwtAuthFilter = jwtAuthFilter;
this.authenticationProvider = authenticationProvider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Désactive CSRF pour les API stateless
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/auth/**").permitAll() // Autorise les points d'authentification
.anyRequest().authenticated() // Toutes les autres requêtes nécessitent une authentification
)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Pas de gestion de session
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // Ajoute notre filtre JWT
return http.build();
}
}
Il faut également exposer l'AuthenticationManager via un Bean pour pouvoir l'injecter et l'utiliser lors de l'authentification initiale :
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
3. Le filtre JWT personnalisé
Ce filtre intercepte chaque requête pour extraire et valider le JWT. Si le token est valide, il configure le contexte de sécurité de Spring.
@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(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userEmail = jwtService.extractUsername(jwt); // Extrait le nom d'utilisateur du JWT
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken); // Met à jour le contexte de sécurité
}
}
filterChain.doFilter(request, response);
}
}
Implémentation de la génération et validation JWT
1. Service JWT pour la génération et validation
Ce service centralise la logique de création, d'extraction des informations et de validation des JWTs.
@Service
public class JwtService {
@Value("${application.security.jwt.secret-key}")
private String secretKey;
@Value("${application.security.jwt.expiration}")
private long jwtExpiration;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts
.builder()
.setClaims(extraClaims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
La clé secrète (secretKey) et la durée d'expiration (jwtExpiration) doivent être configurées dans application.properties ou application.yml. La clé secrète doit être suffisamment longue et complexe.
2. Service d'authentification
Ce service gère la logique métier d'authentification, utilisant l'AuthenticationManager pour vérifier les identifiants et le JwtService pour générer le token.
@Service
public class AuthenticationService {
private final UserRepository repository; // Exemple de UserRepository
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;
public AuthenticationService(UserRepository repository, PasswordEncoder passwordEncoder, JwtService jwtService, AuthenticationManager authenticationManager) {
this.repository = repository;
this.passwordEncoder = passwordEncoder;
this.jwtService = jwtService;
this.authenticationManager = authenticationManager;
}
public AuthenticationResponse register(RegisterRequest request) {
var user = User.builder()
.firstname(request.getFirstname())
.lastname(request.getLastname())
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.role(Role.USER) // Définir un rôle par défaut
.build();
repository.save(user);
var jwtToken = jwtService.generateToken(user);
return new AuthenticationResponse(jwtToken);
}
public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
var user = repository.findByEmail(request.getEmail())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
var jwtToken = jwtService.generateToken(user);
return new AuthenticationResponse(jwtToken);
}
}
Point de vue : développeur full stack à Dakar
Pour un développeur Full Stack comme Laty Gueye Samba, travaillant sur des systèmes comme des applications métier complexes ou des plateformes de gestion des risques à haute performance, la maîtrise de Spring Security avancée avec JWT représente un avantage concurrentiel réel sur le marché technologique africain, en pleine expansion. Cela permet de concevoir des architectures plus résilientes, sécurisées et adaptées aux environnements distribués, où la sécurité des API est un enjeu majeur pour l'intégration de multiples services front-end (Angular, React) et back-end.
Conclusion
L'authentification sans état avec JWT et Spring Security 6 sous Spring Boot 3.x offre une solution puissante et flexible pour sécuriser les API modernes. Elle permet une meilleure scalabilité, une gestion simplifiée des sessions et une intégration facile avec diverses applications clientes, y compris les applications Angular ou les applications mobiles. Laty Gueye Samba, en tant qu'Expert Java Spring Boot Angular, applique régulièrement ces principes pour concevoir des applications robustes et sécurisées, répondant aux exigences des projets les plus complexes au Sénégal et au-delà.
Cette approche est un élément fondamental de la construction d'architectures microservices résilientes et sécurisées. Pour approfondir ces concepts et implémenter une sécurité API de pointe, il est recommandé de consulter la documentation officielle de Spring Security et la documentation JWT.
À 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