Retour aux articles

Sécuriser une API REST Spring Boot 3.x avec JWT et Spring Security 6

Sécuriser une API REST Spring Boot 3.x avec JWT et Spring Security 6 | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular
Sécuriser une API REST Spring Boot 3.x avec JWT et Spring Security 6 | Blog Laty Gueye Samba

Sécuriser une API REST Spring Boot 3.x avec JWT et Spring Security 6

Dans le paysage actuel du développement logiciel, la sécurité des API REST est primordiale. Les applications modernes, qu'il s'agisse de microservices ou de monolithes, s'appuient fortement sur des échanges de données sécurisés. Une API mal protégée peut entraîner des brèches de données, des accès non autorisés et une perte de confiance significative pour les utilisateurs. C'est pourquoi la mise en œuvre de mécanismes d'authentification et d'autorisation robustes est une tâche critique pour tout développeur.

Le framework Spring Boot, associé à Spring Security, offre une suite puissante pour construire des applications Java sécurisées. La version 3.x de Spring Boot et la version 6 de Spring Security apportent des améliorations significatives en termes de performance et de configuration. Pour les API REST stateless, le JSON Web Token (JWT) s'est imposé comme un standard pour l'authentification et l'échange d'informations de manière sécurisée. Cet article explore comment un Développeur Full Stack comme Laty Gueye Samba, basé à Dakar, aborderait la sécurisation d'une API REST Spring Boot 3.x avec Spring Security 6 JWT.

La maîtrise de la sécurisation des API est une compétence indispensable, notamment pour les développeurs travaillant sur des projets d'envergure, que ce soit des applications métier complexes ou des systèmes ERP. L'objectif est de fournir une feuille de route claire pour intégrer efficacement JWT et Spring Security 6 afin de créer une API REST sécurisée.

Comprendre JWT et Spring Security 6 pour une API REST sécurisée

Avant de plonger dans l'implémentation, il est essentiel de comprendre les fondements de JWT et Spring Security 6.

Qu'est-ce que JWT ?

Un JSON Web Token (JWT) est une norme ouverte (RFC 7519) qui définit une manière compacte et auto-contenue de transmettre des informations 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 est généralement utilisé pour l'autorisation : une fois l'utilisateur connecté, le serveur génère un JWT et le renvoie au client. Chaque requête ultérieure du client inclura ce JWT, permettant au serveur de vérifier l'identité de l'utilisateur et d'autoriser l'accès aux ressources protégées sans avoir à interroger une base de données à chaque fois. Sa nature stateless est particulièrement adaptée aux architectures d'API REST.

Spring Security 6 : Les nouveautés pour une API REST sécurisée

Spring Security 6, en tandem avec Spring Boot 3.x, apporte des améliorations majeures. La configuration est devenue plus idiomatique et utilise davantage les expressions lambda, rendant le code plus concis et lisible. Il met également l'accent sur les architectures réactives. Pour la sécurisation d'une API REST, Spring Security gère l'authentification (vérification de l'identité) et l'autorisation (accès aux ressources). L'intégration de JWT se fait généralement en ajoutant un filtre personnalisé dans la chaîne de filtres de Spring Security qui intercepte chaque requête pour valider le token.

Mise en œuvre : Configuration de Spring Security 6 pour JWT

La première étape consiste à ajouter les dépendances nécessaires dans le fichier pom.xml du projet Maven.


<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Boot Starter Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- JWT Library (JJWT) -->
    <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>
    <!-- Lombok (optionnel, pour la concision) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- Autres dépendances... -->
</dependencies>

Configuration de la chaîne de filtres Spring Security

Pour une API REST stateless, il est crucial de désactiver la gestion de session de Spring Security et de configurer une entrée d'authentification personnalisée pour les requêtes non autorisées. La configuration s'effectue généralement dans une classe annotée @Configuration et @EnableWebSecurity.


package com.example.security.config;

import com.example.security.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.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
@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 CSRF pour les API REST
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // Autorise l'accès public à l'API d'authentification
                .anyRequest().authenticated() // Toutes les autres requêtes nécessitent une authentification
            )
            .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // Pas de gestion de session
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // Ajoute le filtre JWT avant le filtre d'authentification par mot de passe

        return http.build();
    }
}

Intégration du filtre JWT et gestion de l'authentification

L'implémentation d'un filtre JWT est au cœur de la sécurisation. Ce filtre interceptera chaque requête, extraira le JWT du header, le validera et définira le contexte de sécurité de Spring Security.

Le filtre d'authentification JWT

Ce filtre sera responsable de l'extraction, de la validation du token et de la configuration du contexte de sécurité.


package com.example.security.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;
    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);
    }
}

Service JWT pour la génération et validation

Un service dédié gérera les opérations liées au JWT (génération, extraction des informations, validation).


package com.example.security.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);
    }
}

Contrôleur d'authentification

Un endpoint d'authentification (login) est nécessaire pour permettre aux utilisateurs d'obtenir un JWT après avoir fourni leurs identifiants.


package com.example.security.auth;

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; // Assurez-vous d'avoir un AuthenticationService injecté

    @PostMapping("/register")
    public ResponseEntity<AuthenticationResponse> register(
        @RequestBody RegisterRequest request
    ) {
        // Logique d'enregistrement (création d'utilisateur et génération de JWT)
        return ResponseEntity.ok(service.register(request));
    }

    @PostMapping("/authenticate")
    public ResponseEntity<AuthenticationResponse> authenticate(
        @RequestBody AuthenticationRequest request
    ) {
        // Logique d'authentification (validation des identifiants et génération de JWT)
        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 hospitalière ou des applications de gestion des risques, la maîtrise de la sécurisation des API REST avec JWT et Spring Security représente un avantage concurrentiel réel sur le marché technologique africain, en pleine expansion. La capacité à construire des systèmes robustes et sécurisés est une compétence très recherchée par les entreprises locales et internationales opérant à Dakar et au-delà.

Conclusion

La sécurisation d'une API REST Spring Boot 3.x avec JWT et Spring Security 6 est une tâche complexe mais essentielle. Cet article a détaillé les étapes clés pour configurer et intégrer ces technologies, en soulignant l'importance d'une approche stateless et d'une gestion rigoureuse des tokens. La mise en œuvre d'une sécurité robuste garantit non seulement la protection des données, mais aussi la fiabilité et la confiance des utilisateurs finaux.

Pour un Développeur Full Stack Dakar Sénégal comme Laty Gueye Samba, l'expertise en Java Spring Boot Angular combinée à une solide compréhension des mécanismes de sécurité comme Spring Security 6 JWT est un atout majeur pour livrer des applications de qualité supérieure. Il est fortement recommandé de consulter la documentation officielle pour approfondir ces concepts et rester à jour avec les meilleures pratiques.

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