Retour aux articles

Implémentation de Spring Security 6 avec JWT pour sécuriser les API REST Spring Boot 3

Implémentation de Spring Security 6 avec JWT pour sécuriser les API REST Spring Boot 3 | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular

Dans l'écosystème du développement web moderne, la sécurisation des API REST est une préoccupation majeure, particulièrement pour les applications critiques et les systèmes d'information. Avec l'évolution rapide des frameworks, il est impératif pour les développeurs de maîtriser les dernières versions des outils de sécurité. Cet article explore l'implémentation de Spring Security 6 avec JSON Web Tokens (JWT) pour garantir la robustesse et la scalabilité des API REST construites avec Spring Boot 3.

L'utilisation de Spring Security 6, couplée à Spring Boot 3, offre une approche moderne et puissante pour gérer l'authentification et l'autorisation. Les JWT, en tant que mécanisme d'authentification sans état, s'intègrent parfaitement dans cette architecture, permettant des services distribués et une meilleure performance. Laty Gueye Samba, un Développeur Full Stack expert en Java Spring Boot et Angular basé à Dakar, Sénégal, souligne régulièrement l'importance d'une sécurité API bien implémentée pour les applications d'entreprise.

Ce guide technique détaillera les étapes clés pour configurer Spring Security 6 et mettre en œuvre l'authentification basée sur JWT, fournissant ainsi une base solide pour la création d'API sécurisées et performantes. L'objectif est de fournir un aperçu concret et des exemples de code pour faciliter l'intégration de ces technologies essentielles.

Principes fondamentaux de Spring Security 6 et JWT

Spring Security est un framework puissant qui fournit des fonctionnalités d'authentification, d'autorisation et d'autres fonctionnalités de sécurité pour les applications Java. Avec Spring Security 6, plusieurs améliorations et simplifications ont été apportées, notamment une configuration plus moderne et basée sur des composants, tirant parti de Java 17 et de Spring Framework 6.

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. Les JWT sont souvent utilisés pour l'authentification et l'autorisation dans les API RESTful en raison de leur nature sans état. Un serveur peut émettre un JWT après une authentification réussie, et le client peut ensuite inclure ce token dans les en-têtes de ses requêtes subséquentes pour accéder aux ressources protégées, sans avoir besoin de sessions côté serveur.

Les JWT sont composés de trois parties séparées par des points :

  • Header : Contient le type de token (JWT) et l'algorithme de hachage utilisé (par exemple, HMAC SHA256 ou RSA).
  • Payload : Contient les "claims" (revendications), qui sont des déclarations sur l'entité (généralement l'utilisateur) et des données supplémentaires. Cela peut inclure des claims enregistrés (iss, exp, sub), publics ou privés.
  • Signature : Créée en prenant le header encodé, le payload encodé, un secret, et l'algorithme spécifié dans le header. Cette signature est utilisée pour vérifier que le token n'a pas été altéré en transit et qu'il provient d'une source fiable.
L'approche sans état des JWT est particulièrement avantageuse pour les architectures de microservices et les applications mobiles, car elle élimine le besoin de maintenir des sessions complexes côté serveur.

Configuration de Spring Security 6 pour JWT

L'intégration de JWT avec Spring Security 6 implique plusieurs étapes de configuration. La première consiste à définir la chaîne de filtres de sécurité (`SecurityFilterChain`) qui interceptera les requêtes HTTP et appliquera les règles d'authentification et d'autorisation. Une classe de configuration, souvent annotée avec @Configuration et @EnableWebSecurity, est nécessaire.

Voici un exemple de configuration de base pour Spring Security, désactivant la protection CSRF (généralement inutile pour les API REST sans état), configurant la gestion de session sans état, et définissant des règles d'autorisation pour les endpoints :


package com.laty.samba.config;

import com.laty.samba.security.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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable) // Désactive la protection CSRF
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // Autorise les requêtes vers les endpoints d'authentification
                .anyRequest().authenticated() // Toutes les autres requêtes nécessitent une authentification
            )
            .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Gestion de session sans état
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // Ajoute le filtre JWT avant le filtre d'authentification par nom d'utilisateur/mot de passe

        return http.build();
    }
}

Le JwtAuthenticationFilter est un filtre personnalisé qui sera responsable de l'extraction et de la validation du JWT des requêtes entrantes. Il est crucial de le placer avant le UsernamePasswordAuthenticationFilter pour que les requêtes avec JWT soient traitées en priorité. L'AuthenticationProvider est quant à lui configuré pour utiliser un UserDetailsService et un PasswordEncoder.


// Exemple de JwtAuthenticationFilter (extrait)
package com.laty.samba.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
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;
    private final 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; // Ou ID utilisateur

        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);
    }
}

Implémentation de l'authentification et de la génération de JWT

Le processus d'authentification et de génération de JWT commence généralement par un endpoint de connexion où un utilisateur fournit ses identifiants. Le serveur utilise alors le AuthenticationManager de Spring Security pour valider ces identifiants.

Un JwtService ou JwtUtil est une classe clé qui gère la création, la validation et l'extraction des informations des JWT. Voici un exemple simplifié :


// Exemple de JwtService (extrait)
package com.laty.samba.security;

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 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);
    }
}

Le AuthenticationController gère les requêtes de connexion et d'enregistrement. Lors d'une connexion réussie, il génère un JWT et le renvoie au client.


// Exemple d'AuthController (extrait)
package com.laty.samba.controller;

import com.laty.samba.dto.AuthenticationRequest;
import com.laty.samba.dto.AuthenticationResponse;
import com.laty.samba.dto.RegisterRequest;
import com.laty.samba.service.AuthenticationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {

    private final AuthenticationService service;

    @PostMapping("/register")
    public ResponseEntity<AuthenticationResponse> register(
            @RequestBody RegisterRequest request
    ) {
        return ResponseEntity.ok(service.register(request));
    }

    @PostMapping("/authenticate")
    public ResponseEntity<AuthenticationResponse> authenticate(
            @RequestBody AuthenticationRequest request
    ) {
        return ResponseEntity.ok(service.authenticate(request));
    }
}

Point de vue : développeur full stack à Dakar

Pour un développeur travaillant sur des systèmes comme des applications de gestion des risques, des projets de gestion hospitalière, ou des applications métier complexes, la maîtrise de Spring Security 6 avec JWT représente un avantage concurrentiel réel sur le marché technologique africain, en pleine expansion. L'adoption de ces technologies permet de construire des architectures robustes et sécurisées, répondant aux exigences croissantes des entreprises locales et internationales. Laty Gueye Samba, Développeur Full Stack à Dakar, observe que cette compétence est fondamentale pour la mise en œuvre de solutions performantes et fiables.

Conclusion

La sécurisation des API REST est un pilier essentiel du développement moderne d'applications. L'implémentation de Spring Security 6 avec JWT fournit une solution robuste, flexible et sans état pour protéger les ressources de vos API Spring Boot 3. Cette combinaison permet de gérer efficacement l'authentification et l'autorisation, tout en offrant une expérience utilisateur fluide et une évolutivité accrue.

En suivant les principes et les exemples de code décrits dans cet article, les développeurs peuvent établir une base de sécurité solide pour leurs projets. Laty Gueye Samba, Développeur Full Stack expert en Java Spring Boot et Angular, encourage l'adoption de ces pratiques pour bâtir des systèmes résilients face aux menaces de sécurité actuelles. La maîtrise de Spring Security et des JWT est une compétence indispensable pour tout Développeur Full Stack aspirant à créer des applications d'entreprise de haute qualité à Dakar et au-delà.

Pour approfondir vos connaissances, il est recommandé de consulter les ressources officielles :

À 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