Retour aux articles

Authentification robuste avec JWT et Spring Security 6 dans Spring Boot 3.x

Authentification robuste avec JWT et Spring Security 6 dans Spring Boot 3.x | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular
Authentification robuste avec JWT et Spring Security 6 dans Spring Boot 3.x

Dans l'écosystème du développement web moderne, la sécurité des applications est une préoccupation primordiale, particulièrement pour les API REST qui alimentent les architectures distribuées et les clients mobiles ou SPA. L'authentification robuste est le pilier central de cette sécurité. Cet article explore comment mettre en œuvre une authentification efficace et sans état à l'aide de JWT (JSON Web Tokens) et de Spring Security 6 au sein d'applications Spring Boot 3.x.

L'intégration de ces technologies permet de bâtir des systèmes d'authentification performants, adaptés aux exigences des applications distribuées et microservices. Pour un développeur Full Stack expert en Java Spring Boot et Angular comme Laty Gueye Samba, basé à Dakar, la maîtrise de ces concepts est essentielle pour construire des solutions résilientes et sécurisées, qu'il s'agisse de systèmes ERP complexes ou d'applications métier critiques.

La transition vers Spring Security 6, en phase avec Spring Boot 3.x sécurité et Java 17+, introduit des optimisations et une API plus moderne, rendant la configuration de l'authentification API REST plus fluide. L'objectif est de comprendre comment ces outils s'articulent pour fournir un mécanisme d'authentification et d'autorisation qui soit à la fois puissant, flexible et maintenable.

Les Fondamentaux : JWT et Spring Security 6

JWT : L'approche Stateless de l'Authentification

Le JSON Web Token (JWT) est une norme ouverte (RFC 7519) qui définit une manière compacte et sécurisée de transmettre des informations entre parties sous forme d'objet JSON. Il est particulièrement adapté aux architectures microservices et aux APIs REST car il est stateless. Cela signifie que le serveur n'a pas besoin de maintenir un état de session pour l'utilisateur, toutes les informations nécessaires étant contenues dans le token lui-même.

Un JWT se compose de trois parties séparées par des points : un en-tête (header), une charge utile (payload) et une signature. La signature assure l'intégrité du token, garantissant qu'il n'a pas été altéré depuis son émission. Lors de l'authentification, après la vérification des identifiants, un serveur génère un JWT qui est ensuite renvoyé au client. Ce client inclut le JWT dans les en-têtes de chaque requête subséquente (généralement dans l'en-tête Authorization: Bearer <token>) pour prouver son identité et ses permissions.

Spring Security 6 : La Nouvelle Génération

Spring Security 6 apporte des améliorations significatives par rapport à ses prédécesseurs, notamment une meilleure intégration avec Spring Boot 3.x et Java 17+. L'architecture a été affinée, avec une gestion plus explicite des filtres de sécurité via la classe SecurityFilterChain. Cette version met l'accent sur la configurabilité et la flexibilité, permettant aux développeurs de personnaliser précisément la chaîne de filtres pour des cas d'utilisation spécifiques, y compris l'authentification JWT Spring Security 6.

Une des principales avancées réside dans la simplification de la configuration des endpoints publics et privés, et une approche plus moderne pour l'intégration de mécanismes d'authentification personnalisés. Les développeurs Full Stack à Dakar, confrontés à des exigences de sécurité variées dans des applications comme des plateformes de gestion hospitalière ou des applications de gestion des risques, trouvent dans Spring Security 6 un cadre robuste pour répondre à ces défis.

Mise en Œuvre de l'Authentification JWT avec Spring Security 6 dans Spring Boot 3.x

L'intégration de JWT avec Spring Boot 3.x sécurité et Spring Security 6 nécessite plusieurs composants clés. Voici une approche structurée pour y parvenir.

1. Dépendances Maven/Gradle

Il est impératif d'ajouter les dépendances nécessaires à votre projet Spring Boot :


<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<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>
<!-- Spring Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
    

2. Configuration de Spring Security (SecurityFilterChain)

La configuration principale se fait via une classe de configuration étendant WebSecurityConfigurerAdapter (dépréciée) ou, pour Spring Security 6, en exposant un bean SecurityFilterChain. Il faut désactiver le CSRF, la gestion de session et configurer les règles d'autorisation pour les requêtes.


package com.latysamba.security;

import com.latysamba.jwt.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Permet la sécurité au niveau des méthodes (@PreAuthorize)
@RequiredArgsConstructor
public class SecurityConfiguration {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // Désactiver CSRF pour les APIs REST stateless
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll() // Public endpoints
                .anyRequest().authenticated() // Toutes les autres requêtes nécessitent une authentification
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // Pas de gestion de session HTTP
            )
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // Ajouter notre filtre JWT

        return http.build();
    }
}
    

3. Implémentation du Filtre JWT (JwtAuthenticationFilter)

Ce filtre interceptera chaque requête pour extraire et valider le JWT. Si le token est valide, il authentifie l'utilisateur dans le contexte de sécurité de Spring.


package com.latysamba.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService; // Service pour la gestion des JWT (extraction, validation)
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            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 l'email de l'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);
            }
        }
        filterChain.doFilter(request, response);
    }
}
    

4. Service JWT et UserDetailsService

Le JwtService encapsule la logique de génération, d'extraction et de validation des JWT. Le UserDetailsService (une interface de Spring Security) est responsable de charger les détails de l'utilisateur à partir d'une source de données (base de données, LDAP, etc.).


package com.latysamba.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@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 (secret-key) et l'expiration (expiration) sont généralement configurées dans application.properties ou application.yml. La clé doit être suffisamment longue et sécurisée.

5. Configuration de l'Authentification (AuthenticationProvider)

Cette configuration définit comment les utilisateurs sont authentifiés (par exemple, en comparant un mot de passe haché).


package com.latysamba.config;

import com.latysamba.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {

    private final UserRepository repository; // Votre repository d'utilisateurs

    @Bean
    public UserDetailsService userDetailsService() {
        return username -> repository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
    

Point de vue : développeur full stack à Dakar

Pour un développeur Full Stack basé à Dakar, travaillant sur des systèmes comme des applications de gestion des ressources humaines ou des plateformes e-commerce, la maîtrise de l'authentification stateless avec JWT et Spring Security 6 représente un avantage concurrentiel réel sur le marché technologique africain, en pleine expansion. L'adoption de ces pratiques modernes permet de construire des architectures résilientes et sécurisées, répondant aux exigences des environnements de production complexes.

Conclusion

L'implémentation de l'authentification avec JWT et Spring Security 6 dans Spring Boot 3.x fournit une solution robuste, performante et flexible pour sécuriser les API REST. En adoptant une approche stateless, les applications gagnent en scalabilité et en simplicité, des atouts majeurs pour les architectures modernes basées sur des microservices. Les exemples de code présentés ici illustrent les bases nécessaires pour mettre en œuvre une sécurité d'API de haute qualité.

La capacité à architecturer et à coder des systèmes d'authentification robuste est une compétence clé pour tout Développeur Full Stack, et Laty Gueye Samba, Expert Java Spring Boot Angular à Dakar, continue d'explorer et de partager les meilleures pratiques pour bâtir des applications toujours plus sécurisées et performantes.

Pour approfondir vos connaissances, il est recommandé de consulter la documentation officielle :

À 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