Tutoriel Complet sur Spring Security : Créez une API REST sécurisée pour une To-Do List

+5 min de lecture
Tutoriel Complet sur Spring Security : Créez une API REST sécurisée pour une To-Do List
Y
Par YACOUBA KONE

Introduction

Spring Security est le framework de sécurité de Spring, conçu pour sécuriser les applications Spring Boot. Ce tutoriel détaillé vous permettra de :

  1. Comprendre les concepts fondamentaux de Spring Security.
  2. Créer une API REST sécurisée pour une to-do list.
  3. Utiliser PostgreSQL comme base de données.

Pré-requis :

  • Connaissances de base en Java et Spring Boot.
  • PostgreSQL installé.
  • Outils recommandés : IntelliJ IDEA ou VS Code.

Voyons comment sécuriser votre application Spring Boot avec Spring Security ! 😊

1. Concepts Fondamentaux de Spring Security (Explication Détaillée)

Spring Security est une solution puissante et flexible pour sécuriser les applications Java/Spring. Avant de plonger dans le projet pratique, il est essentiel de comprendre les principaux composants et concepts suivants :

1.1 SecurityContext

Le SecurityContext est un conteneur central utilisé par Spring Security pour stocker les informations de sécurité de la session en cours.

  • Contenu principal :
    Le SecurityContext contient un objet Authentication qui représente l'utilisateur authentifié ou en cours d'authentification.

  • Pourquoi est-il important ?
    Il permet de maintenir les informations sur l'utilisateur courant tout au long du traitement de la requête.

  • Exemple d'accès :
    Vous pouvez accéder au SecurityContext en appelant :

    SecurityContextHolder.getContext().getAuthentication();
    
  • Cas pratique : Lorsque vous avez besoin de récupérer l'utilisateur actuellement connecté (par exemple, pour charger ses tâches dans une to-do list), vous pouvez utiliser le SecurityContext.

1.2 AuthenticationManager

Le AuthenticationManager est le cœur du processus d'authentification dans Spring Security.

  • Rôle :
    Il traite les demandes d'authentification en vérifiant les informations d'identification de l'utilisateur (nom d'utilisateur, mot de passe, etc.) par rapport à une source de données (comme une base de données ou un système LDAP).

  • Fonctionnement :

    • Lorsqu'un utilisateur soumet ses informations d'authentification, elles sont encapsulées dans un objet Authentication.
    • Le AuthenticationManager tente ensuite de les authentifier à l'aide des stratégies définies.
  • Personnalisation :
    Vous pouvez personnaliser l'AuthenticationManager pour intégrer vos propres stratégies d'authentification.

  • Exemple de configuration :

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("admin")
            .password(passwordEncoder().encode("password"))
            .roles("ADMIN");
    }
    

1.3 UserDetails et UserDetailsService

  • UserDetails :
    C'est une interface qui représente un utilisateur dans Spring Security. Elle définit les informations nécessaires pour l'authentification et l'autorisation, telles que :

    • Nom d'utilisateur.
    • Mot de passe (souvent encodé).
    • Rôles ou permissions (via GrantedAuthority).
  • UserDetailsService :
    Une interface utilisée pour charger les informations utilisateur depuis une source de données (comme une base de données).

  • Rôle dans l'application :
    Le UserDetailsService est appelé par Spring Security pour rechercher et vérifier les informations utilisateur lors de l'authentification.

  • Exemple de personnalisation :

    @Service
    public class CustomUserDetailsService implements UserDetailsService {
        @Autowired
        private UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepository.findByUsername(username)
                    .orElseThrow(() -> new UsernameNotFoundException("User not found"));
            return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    Collections.singletonList(new SimpleGrantedAuthority(user.getRole()))
            );
        }
    }
    

1.4 GrantedAuthority

  • Rôle :
    Une GrantedAuthority représente une permission ou un rôle attribué à un utilisateur (par exemple, ROLE_USER, ROLE_ADMIN).
    Ces permissions déterminent ce qu'un utilisateur peut faire dans l'application.

  • Exemple d'utilisation :
    Lors de la création d'un utilisateur, vous pouvez attribuer un rôle :

    new SimpleGrantedAuthority("ROLE_USER");
    

1.5 PasswordEncoder

Spring Security n'enregistre jamais de mots de passe en texte clair. Au lieu de cela, il utilise un PasswordEncoder pour :

  1. Encoder les mots de passe avant de les stocker.
  2. Vérifier les mots de passe lors de l'authentification.
  • Exemple avec BCryptPasswordEncoder :

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
  • Pourquoi est-ce important ?
    Le chiffrement garantit que même si la base de données est compromise, les mots de passe restent sécurisés.

1.6 Filtre de Sécurité (Filter Chain)

Spring Security utilise une chaîne de filtres (filter chain) pour gérer les requêtes HTTP.

  • Fonctionnement :

    1. Chaque requête passe par une série de filtres.
    2. Ces filtres effectuent des tâches spécifiques comme vérifier les jetons JWT, authentifier les utilisateurs, ou appliquer des règles de sécurité.
  • Exemple de filtres courants :

    • UsernamePasswordAuthenticationFilter : Authentifie les utilisateurs via un formulaire de connexion.
    • JwtAuthenticationFilter : Vérifie la validité d'un token JWT.

1.7. Étapes Préparatoires : Pourquoi Ces Concepts Sont-ils Essentiels ?

Ces concepts sont cruciaux pour comprendre comment sécuriser une application Spring Boot. Ils serviront de fondation pour notre projet pratique, où nous implémenterons :

  1. Un système d'authentification utilisant des noms d'utilisateur et mots de passe.
  2. Un contrôle d'accès basé sur les rôles pour protéger les différentes API.
  3. Un chiffrement des mots de passe avec PasswordEncoder.
  4. Une sécurité basée sur JWT pour des interactions sécurisées entre le client et le serveur.

2. Configuration de l'Environnement

Nous allons construire une API REST Todo List sécurisée avec Spring Security et Postgres, en suivant les étapes détaillées. Chaque section inclura les explications, la configuration et le code pour chaque fichier.

Plan du Projet

  1. Initialisation du Projet Spring Boot.
  2. Configuration de Spring Security.
  3. Mise en place du modèle de données avec PostgreSQL.
  4. Authentification et Autorisation (JWT).
  5. Création des Endpoints API REST (CRUD Todo List).
  6. Test et sécurisation des routes.

1. Initialisation du Projet Spring Boot

1.1 Créez le projet avec Spring Initializr

Allez sur Spring Initializr et configurez votre projet comme suit :

  • Nom : todo-list-api.
  • Dependencies :
    • Spring Web : Pour créer les API REST.
    • Spring Security : Pour gérer la sécurité.
    • Spring Data JPA : Pour interagir avec PostgreSQL.
    • PostgreSQL Driver : Pour connecter la base de données.
    • Lombok : Pour réduire le code boilerplate.

** Organisez votre projet comme suit : **

src/main/java/com/example/todolist/
├── controller
├── model
├── repository
├── service
├── security
└── config

Téléchargez le projet, extrayez-le, et ouvrez-le dans votre IDE préféré.

1.2 Configuration du fichier application.properties

Modifiez le fichier src/main/resources/application.properties pour connecter PostgreSQL.

# Configurations de PostgreSQL
spring.datasource.url=jdbc:postgresql://localhost:5432/todo_db
spring.datasource.username=postgres
spring.datasource.password=mot_de_passe

# Configuration JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

# Port du serveur
server.port=8080

2. Configuration de Spring Security

Spring Security est au cœur de la sécurisation de notre API. Voici comment le configurer étape par étape. pour commencer nous allons creer un model User

**2.0 Création du modèle User et du Repository **

Créez une classe User pour stocker les informations des utilisateurs.

Fichier : User.java

package com.example.todo.model;

import jakarta.persistence.*;
import lombok.Data;

import java.util.Set;

@Entity
@Data
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    private Set<String> roles;
}

Fichier : UserRepository.java

    package com.example.todo.repository;
    
    import com.example.todo.model.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import java.util.Optional;
    
    public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

2.1 Création d'un fichier de configuration

Ajoutez une classe pour configurer Spring Security.

Fichier : SecurityConfig.java

package com.example.todo.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic();
        return http.build();
    }
}
  • Explications :
    • csrf().disable() : Désactive la protection CSRF (utile pour les API REST).
    • requestMatchers("/api/auth/**").permitAll() : Permet un accès public aux endpoints d'authentification.
    • anyRequest().authenticated() : Toutes les autres routes nécessitent une authentification.

2.2 Service Utilisateur Personnalisé

Créez une implémentation de UserDetailsService pour charger les utilisateurs depuis la base de données.

Fichier : CustomUserDetailsService.java

package com.example.todo.security;

import com.example.todo.model.User;
import com.example.todo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.getRoles()
        );
    }
}

3. Authentification et Autorisation avec JWT

L'authentification basée sur JWT (JSON Web Token) permet de sécuriser les API REST de manière stateless. Un utilisateur authentifié reçoit un token qu'il envoie dans chaque requête pour prouver son identité.

3.1 Ajout des Dépendances JWT

Ajoutez la dépendance suivante dans le fichier pom.xml :

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.6</version>
</dependency>

Mettez à jour les dépendances avec Maven.

3.2 Création d'un Service JWT

Ajoutez une classe pour générer et valider les tokens JWT.

Fichier : JwtService.java

package com.example.todo.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.function.Function;

@Service
public class JwtService {

    private static final String SECRET_KEY = "secret-key";

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

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY.getBytes())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 heures
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes())
                .compact();
    }

    public boolean isTokenValid(String token, String username) {
        final String extractedUsername = extractUsername(token);
        return extractedUsername.equals(username) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
}

3.3 Configuration de JWT dans Spring Security

Ajoutez un filtre pour valider les tokens JWT dans chaque requête.

Fichier : JwtAuthenticationFilter.java

package com.example.todo.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
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
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private 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 username;

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        jwt = authHeader.substring(7);
        username = jwtService.extractUsername(jwt);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValid(jwt, userDetails.getUsername())) {
                var authToken = new org.springframework.security.authentication.UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

3.4 Mise à jour de la configuration Spring Security

Ajoutez le filtre JWT dans la classe SecurityConfig.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
    http.csrf().disable()
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated()
        )
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

La partie JWT est prête! Vous pouvez maintenant tester les routes d'inscription (/api/auth/register) et de connexion (/api/auth/login) pour obtenir un token.

5. Création des Endpoints pour la Gestion des Tâches

Maintenant que l'authentification est en place, nous allons créer les endpoints REST pour gérer les tâches (TODOs). Ces endpoints seront sécurisés : seuls les utilisateurs authentifiés pourront y accéder.

5.1 Création de l'Entité Task

Définissons une entité JPA pour représenter les tâches.

Fichier : Task.java

package com.example.todo.model;

import jakarta.persistence.*;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@Entity
@Table(name = "tasks")
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String description;

    private boolean completed;

    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
}

5.2 Création du Repository pour Task

Ajoutons un repository pour effectuer les opérations CRUD sur les tâches.

Fichier : TaskRepository.java

package com.example.todo.repository;

import com.example.todo.model.Task;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface TaskRepository extends JpaRepository<Task, Long> {
    List<Task> findByUserId(Long userId);
}

5.3 Création du Service pour Task

Ajoutons une couche de service pour encapsuler la logique métier des tâches.

Fichier : TaskService.java

package com.example.todo.service;

import com.example.todo.model.Task;
import com.example.todo.model.User;
import com.example.todo.repository.TaskRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class TaskService {

    @Autowired
    private TaskRepository taskRepository;

    public List<Task> getAllTasksForUser(User user) {
        return taskRepository.findByUserId(user.getId());
    }

    public Task createTask(Task task, User user) {
        task.setUser(user);
        task.setCreatedAt(LocalDateTime.now());
        task.setUpdatedAt(LocalDateTime.now());
        return taskRepository.save(task);
    }

    public Task updateTask(Long taskId, Task updatedTask, User user) {
        Task task = taskRepository.findById(taskId)
                .orElseThrow(() -> new RuntimeException("Task not found"));

        if (!task.getUser().getId().equals(user.getId())) {
            throw new RuntimeException("Unauthorized");
        }

        task.setTitle(updatedTask.getTitle());
        task.setDescription(updatedTask.getDescription());
        task.setCompleted(updatedTask.isCompleted());
        task.setUpdatedAt(LocalDateTime.now());
        return taskRepository.save(task);
    }

    public void deleteTask(Long taskId, User user) {
        Task task = taskRepository.findById(taskId)
                .orElseThrow(() -> new RuntimeException("Task not found"));

        if (!task.getUser().getId().equals(user.getId())) {
            throw new RuntimeException("Unauthorized");
        }

        taskRepository.delete(task);
    }
}

5.4 Création du Contrôleur pour Task

Ajoutons un contrôleur pour exposer les endpoints REST pour la gestion des tâches.

Fichier : TaskController.java

package com.example.todo.controller;

import com.example.todo.model.Task;
import com.example.todo.model.User;
import com.example.todo.service.TaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/tasks")
public class TaskController {

    @Autowired
    private TaskService taskService;

    @GetMapping
    public List<Task> getAllTasks(@AuthenticationPrincipal User user) {
        return taskService.getAllTasksForUser(user);
    }

    @PostMapping
    public Task createTask(@RequestBody Task task, @AuthenticationPrincipal User user) {
        return taskService.createTask(task, user);
    }

    @PutMapping("/{taskId}")
    public Task updateTask(@PathVariable Long taskId, @RequestBody Task task, @AuthenticationPrincipal User user) {
        return taskService.updateTask(taskId, task, user);
    }

    @DeleteMapping("/{taskId}")
    public String deleteTask(@PathVariable Long taskId, @AuthenticationPrincipal User user) {
        taskService.deleteTask(taskId, user);
        return "Task deleted successfully!";
    }
}

5.5 Résultat Final

Voici les endpoints disponibles pour gérer les tâches :

  • GET /api/tasks : Récupère toutes les tâches de l'utilisateur connecté.
  • POST /api/tasks : Crée une nouvelle tâche.
  • PUT /api/tasks/{taskId} : Met à jour une tâche existante.
  • DELETE /api/tasks/{taskId} : Supprime une tâche.

6. Tester l'API

Vous pouvez tester ces endpoints avec Postman ou cURL. Lors de chaque requête, ajoutez un header Authorization avec le token JWT récupéré lors de la connexion.

Exemple d'en-tête :

Authorization: Bearer <votre_token>

7. Documentation des APIs avec Swagger

Spring Boot facilite l'intégration de Swagger pour documenter vos endpoints REST. Swagger permet non seulement de générer une documentation interactive mais également de tester les endpoints directement via une interface web.

7.1 Ajouter les Dépendances Swagger

Ajoutez les dépendances nécessaires dans votre fichier pom.xml.

Fichier : pom.xml

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version> <!-- Vérifiez la dernière version disponible -->
</dependency>

7.2 Configurer Swagger

Dans la plupart des cas, aucune configuration supplémentaire n’est nécessaire avec SpringDoc. Cependant, vous pouvez personnaliser le comportement en créant une classe de configuration.

Fichier : OpenApiConfig.java

package com.example.todo.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("TODO List API")
                        .version("1.0")
                        .description("Documentation des APIs pour la gestion des tâches avec Spring Security"));
    }
}

7.3 Accéder à l’Interface Swagger

Démarrez votre application Spring Boot et rendez-vous à l’adresse suivante dans votre navigateur :

http://localhost:8080/swagger-ui.html

Vous verrez une interface interactive contenant tous les endpoints documentés. Chaque endpoint sera accompagné des méthodes HTTP, des paramètres nécessaires, des réponses possibles, etc.

8. Tester les Endpoints avec Swagger

Swagger vous permet de tester directement les endpoints. Voici un exemple pour utiliser l'interface Swagger :

  1. Authentifiez-vous :

    • Utilisez le endpoint /api/auth/login pour récupérer un token JWT.
    • Copiez le token reçu dans la réponse.
  2. Ajouter le Token JWT :

    • Cliquez sur le bouton "Authorize" dans Swagger.
    • Entrez le token JWT dans le champ sous la forme :
      Bearer <votre_token>
      
  3. Tester les Endpoints :

    • Testez les endpoints comme GET /api/tasks, POST /api/tasks, PUT /api/tasks/{taskId}, etc.
    • Fournissez les données nécessaires via l'interface interactive.

9. Résultat Final dans Swagger

Voici un aperçu des endpoints visibles dans Swagger après configuration :

  • Endpoints d’Authentification :

    • POST /api/auth/register : Enregistrer un nouvel utilisateur.
    • POST /api/auth/login : Se connecter et obtenir un token JWT.
  • Endpoints pour la Gestion des Tâches :

    • GET /api/tasks : Récupérer toutes les tâches de l’utilisateur connecté.
    • POST /api/tasks : Créer une nouvelle tâche.
    • PUT /api/tasks/{taskId} : Mettre à jour une tâche existante.
    • DELETE /api/tasks/{taskId} : Supprimer une tâche.

10. Étapes Suivantes

  • Sécuriser davantage l'application : Ajoutez des rôles (comme ADMIN, USER) pour différencier les autorisations.
  • Améliorer les validations : Utilisez des annotations comme @Valid pour valider les requêtes entrantes.
  • Déployer l'application : Déployez votre application sur un serveur comme Heroku ou AWS.
JavaSpring BootSpring SecurityJWTRESTAPITodo ListTutoriel