Spring Boot

  1. Introduction
    1. Qu’est-ce que Spring Boot ?
    2. Différence avec Spring MVC
    3. Avantages de Spring Boot
    4. Architecture de base
  2. Mise en place
    1. Prérequis (Java, Maven/Gradle, IDE)
    2. Création d’un projet avec Spring Initializr
    3. Structure d’un projet
    4. Configuration de base (application.properties/application.yml)
  3. Fondamentaux
    1. Auto-configuration et starters
    2. Création d’un contrôleur REST
    3. Gestion des dépendances
    4. Démarrage de l’application
  4. API REST
    1. Annotations REST
    2. Gestion des méthodes HTTP
    3. Passage de paramètres
    4. Réponses HTTP et codes de statut
  5. Gestion des données
    1. Introduction à Spring Data JPA
    2. Configuration de la base de données
    3. Création d’entités et repositories
    4. Opérations CRUD avec Spring Data
  6. Vues et Templates
    1. Intégration de Thymeleaf
    2. Création de pages dynamiques
    3. Gestion des formulaires et validation
    4. Comparaison avec JSP
  7. Sécurité
    1. Introduction à Spring Security
    2. Configuration de l’authentification
    3. Sécurisation des endpoints et pages
    4. Gestion des rôles et permissions
  8. Avancées
    1. Gestion des exceptions
    2. Configuration des profils
    3. Internationalisation (i18n)
    4. Utilisation de Spring Boot Actuator
  9. Tests
    1. Tests unitaires avec JUnit et Mockito
    2. Tests d’intégration
    3. Simulation de requêtes
    4. Couverture de code
  10. Déploiement
    1. Construction d’un JAR exécutable
    2. Déploiement sur serveur ou cloud
    3. Configuration des variables d’environnement
    4. Monitoring et logging
  11. Bonnes pratiques
    1. Structuration d’un projet
    2. Optimisation des performances




1. Introduction à Spring Boot

1.1. Qu’est-ce que Spring Boot ?

Spring Boot est une extension du framework Spring qui simplifie le développement d’applications Java en offrant une configuration automatique, des dépendances prépackagées (starters), et un serveur embarqué (Tomcat par défaut). Il est conçu pour réduire le code "boilerplate" et accélérer la mise en production.

Exemple conceptuel :

@SpringBootApplication public class GestionVentesApplication { public static void main(String[] args) { SpringApplication.run(GestionVentesApplication.class, args); } }
  • @SpringBootApplication : Combine @Configuration, @EnableAutoConfiguration, et @ComponentScan.
  • SpringApplication.run : Lance l’application avec un serveur embarqué.
graph TD A[Spring Boot] --> B(Auto-configuration) A --> C[Starters] A --> D[Serveur Embarqué] B --> E[Moins de config] C --> F[Dépendances Simplifiées] D --> G[Tomcat/Jetty]

1.2. Différence entre Spring Boot et Spring MVC classique

Spring MVC nécessite une configuration manuelle (XML ou Java) et un serveur externe (ex. Tomcat déployé séparément). Spring Boot automatise cela avec des conventions et un serveur intégré.

Tableau comparatif :

AspectSpring MVCSpring Boot
ConfigurationManuelleAuto-configurée
ServeurExterne (Tomcat)Embarqué
DépendancesDéfinies manuellementStarters
DémarrageDéploiement WARJAR exécutable
graph TD A[Comparaison] --> B(Spring MVC) A --> C(Spring Boot) B --> D[Config Manuelle] B --> E[Serveur Externe] C --> F[Auto-config] C --> G[Serveur Embarqué]

1.3. Avantages de Spring Boot

Explication :

  • Convention sur configuration : Réduit le besoin de configuration explicite.
  • Starters : Simplifie la gestion des dépendances (ex. spring-boot-starter-web).
  • Serveur embarqué : Pas besoin d’installer un serveur séparé.
  • Production-ready : Outils comme Actuator pour la supervision.

Exemple de starter dans pom.xml :

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
graph TD A[Avantages] --> B(Convention) A --> C[Starters] A --> D[Serveur Embarqué] A --> E[Production-ready] C --> F[Ex: spring-boot-starter-web] E --> G[Actuator]

1.4. Architecture de base d’une application Spring Boot

Une application Spring Boot suit une architecture typique :

  • Couche présentation : Contrôleurs REST ou pages web.
  • Couche métier : Services pour la logique.
  • Couche données : Repositories pour accéder à la base (MySQL ici).
  • Configuration : Gérée via application.properties ou application.yml.

Structure initiale du projet :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com.exemple.ventes/ │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ └── application.properties ├── pom.xml
graph TD A[Architecture] --> B(Présentation) A --> C[Métier] A --> D[Données] A --> E[Configuration] B --> F[Contrôleurs] C --> G[Services] D --> H[Repositories] E --> I[application.properties]

Projet CRUD : Gestion des ventes - Premiers pas

Objectif : Construire une application pour gérer des ventes avec les entités suivantes :

  • Categorie : id, nom, description.
  • Produit : id, idCategorie, nom, prix.
  • Client : id, nom, email, password, status.
  • Commande : id, idProduit, idClient, date, quantite.

Étape actuelle : Comprendre Spring Boot avant de configurer le projet.

Structure envisagée :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com.exemple.ventes/ │ │ │ ├── controller/ │ │ │ ├── model/ │ │ │ ├── repository/ │ │ │ ├── service/ │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ └── application.properties ├── pom.xml

Prochaines étapes : Configurer l’environnement et initialiser le projet (section 2).

2. Mise en place de l’environnement

2.1. Prérequis (Java, Maven/Gradle, IDE comme IntelliJ ou Eclipse)

Pour développer avec Spring Boot, vous avez besoin de :

  • Java : JDK 17 (version moderne recommandée).
  • Maven ou Gradle : Outil de gestion de dépendances (nous utiliserons Maven ici).
  • IDE : IntelliJ IDEA ou Eclipse pour faciliter le développement.

Étapes :

  1. Installez Java (java -version pour vérifier).
  2. Installez Maven (mvn -version pour vérifier).
  3. Téléchargez et configurez un IDE (ex. IntelliJ IDEA Community).
graph TD A[Prérequis] --> B(Java JDK) A --> C(Maven/Gradle) A --> D[IDE] B --> E[Version 17] C --> F[Maven Utilisé] D --> G[IntelliJ/Eclipse]

2.2. Création d’un projet avec Spring Initializr

Spring Initializr est un outil en ligne (ou intégré aux IDE) pour générer un projet Spring Boot avec les dépendances nécessaires. Nous allons inclure les starters pour le web, JPA, MySQL, et Thymeleaf.

Étapes :

  1. Allez sur start.spring.io.
  2. Configurez :
    • Projet : Maven.
    • Langage : Java.
    • Version Spring Boot : 3.2.3 (dernière stable au 28/02/2025, supposée).
    • Group : com.exemple.
    • Artifact : gestion-ventes.
    • Java : 17.
    • Dépendances : Spring Web, Spring Data JPA, MySQL Driver, Thymeleaf.
  3. Générez et téléchargez le ZIP, puis décompressez-le.
graph TD A[Spring Initializr] --> B(Maven) A --> C[Java 17] A --> D[Spring Boot 3.2.3] A --> E[Dépendances] E --> F[Spring Web] E --> G[Spring Data JPA] E --> H[MySQL Driver] E --> I[Thymeleaf]

2.3. Structure d’un projet Spring Boot

Le projet généré suit une structure standard :

  • src/main/java : Code source Java.
  • src/main/resources : Fichiers de configuration et ressources statiques.
  • pom.xml : Gestion des dépendances.

Structure initiale :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/exemple/ventes/ │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ ├── static/ # Fichiers CSS, JS │ │ ├── templates/ # Fichiers Thymeleaf │ │ └── application.properties │ └── test/ ├── pom.xml

Contenu de pom.xml :

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.exemple</groupId> <artifactId>gestion-ventes</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.3</version> <relativePath/> </parent> <properties> <java.version>17</java.version> </properties> <dependencies> <!-- Web pour API REST et Thymeleaf --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- JPA pour la persistance --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- MySQL Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Thymeleaf pour les vues --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- Tests --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
  • <parent> : Hérite des configurations de Spring Boot.
  • Dépendances : Incluent les starters nécessaires.
  • spring-boot-maven-plugin : Permet de générer un JAR exécutable.
graph TD A[Structure] --> B(src/main/java) A --> C[src/main/resources] A --> D[pom.xml] B --> E[GestionVentesApplication] C --> F[application.properties] C --> G[static/] C --> H[templates/] D --> I[Starters]

2.4. Configuration de base (fichier application.properties/application.yml)

Le fichier application.properties configure l’application, notamment la connexion à MySQL.

Configuration MySQL - src/main/resources/application.properties :

properties
# Port de l'application server.port=8080 # Configuration MySQL spring.datasource.url=jdbc:mysql://localhost:3306/gestion_ventes?createDatabaseIfNotExist=true spring.datasource.username=root spring.datasource.password=votre_mot_de_passe spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # Hibernate/JPA spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect # Thymeleaf spring.thymeleaf.cache=false

Étapes pour MySQL :

  1. Installez MySQL (ex. via Docker ou localement).
  2. Créez une base gestion_ventes :
    sql
    CREATE DATABASE gestion_ventes;
  3. Mettez à jour username et password selon votre configuration MySQL.

Explication :

  • spring.datasource.* : Configure la connexion à MySQL.
  • spring.jpa.hibernate.ddl-auto=update : Met à jour le schéma automatiquement.
  • spring.thymeleaf.cache=false : Désactive le cache pour le développement.

Alternative en YAML - application.yml :

yaml
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/gestion_ventes?createDatabaseIfNotExist=true username: root password: votre_mot_de_passe driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect thymeleaf: cache: false
graph TD A[Configuration] --> B(application.properties) A --> C[application.yml] B --> D[Port: 8080] B --> E[MySQL] E --> F[URL] E --> G[Username/Password] B --> H[JPA/Hibernate] H --> I[ddl-auto: update]

Projet CRUD : Gestion des ventes - Configuration initiale

Structure actuelle :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/exemple/ventes/ │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ └── application.properties ├── pom.xml

Classe principale - GestionVentesApplication.java :

package com.exemple.ventes; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GestionVentesApplication { public static void main(String[] args) { SpringApplication.run(GestionVentesApplication.class, args); } }

Test initial :

  1. Lancez MySQL et créez la base gestion_ventes.
  2. Exécutez l’application :
    mvn spring-boot:run
  3. Vérifiez que le serveur démarre sur http://localhost:8080.

3. Les fondamentaux de Spring Boot

3.1. Auto-configuration et starters

Spring Boot utilise l’auto-configuration pour configurer automatiquement les composants en fonction des dépendances (starters) présentes dans le projet. Par exemple, spring-boot-starter-web configure un serveur Tomcat embarqué et Spring MVC, tandis que spring-boot-starter-data-jpa configure Hibernate.

Exemple :
Avec les starters déjà inclus dans pom.xml (web, JPA, MySQL, Thymeleaf), Spring Boot détecte MySQL et configure une datasource sans code supplémentaire.

graph TD A[Auto-configuration] --> B[Détecte Starters] B --> C[spring-boot-starter-web] B --> D[spring-boot-starter-data-jpa] C --> E[Tomcat] C --> F[Spring MVC] D --> G[Hibernate] D --> H[DataSource]

3.2. Création d’un premier contrôleur REST

Un contrôleur REST gère les requêtes HTTP et renvoie des réponses au format JSON. Nous allons créer un contrôleur simple pour tester l’application avant d’implémenter le CRUD.

Contrôleur - src/main/java/com/exemple/ventes/controller/TestController.java :

package com.exemple.ventes.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { @GetMapping("/hello") public String direBonjour() { return "Bonjour depuis Spring Boot !"; } }
  • @RestController : Combine @Controller et @ResponseBody, indiquant que les réponses sont au format JSON (ou texte ici).
  • @GetMapping("/hello") : Mappe la requête GET /hello à cette méthode.

Test :

  1. Lancez l’application avec mvn spring-boot:run.
  2. Ouvrez un navigateur ou utilisez curl : http://localhost:8080/hello.
  3. Résultat attendu : "Bonjour depuis Spring Boot !".
graph TD A[Contrôleur REST] --> B(@RestController) B --> C[JSON/Texte] A --> D[@GetMapping] D --> E[hello] E --> F[Retourne String]

3.3. Gestion des dépendances avec Maven/Gradle

Maven gère les dépendances via pom.xml. Les starters simplifient ce processus en regroupant des bibliothèques courantes. Nous avons déjà configuré les dépendances nécessaires dans la section 2.

Ajout d’une dépendance utile - Lombok (optionnel) :

<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
  • Lombok : Réduit le code boilerplate (getters, setters) avec des annotations comme @Data.
graph TD A[Dépendances] --> B(Maven) B --> C[pom.xml] C --> D[Starters] D --> E[Web] D --> F[JPA] C --> G[Lombok] G --> H[@Data]

3.4. Démarrage de l’application (serveur embarqué : Tomcat, Jetty)

Spring Boot inclut un serveur embarqué (Tomcat par défaut) lancé via SpringApplication.run. Pas besoin de déployer un WAR comme avec Spring MVC classique.

Classe principale (rappel) - GestionVentesApplication.java :

package com.exemple.ventes; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GestionVentesApplication { public static void main(String[] args) { SpringApplication.run(GestionVentesApplication.class, args); } }

Changer de serveur (optionnel, ex. Jetty) :

  1. Exclure Tomcat et ajouter Jetty dans pom.xml :
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency>
graph TD A[Démarrage] --> B(SpringApplication.run) B --> C[Serveur Embarqué] C --> D[Tomcat par défaut] C --> E[Jetty optionnel] A --> F[@SpringBootApplication]

Projet CRUD : Gestion des ventes - Progression

Structure actuelle :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/exemple/ventes/ │ │ │ ├── controller/ │ │ │ │ └── TestController.java │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ └── application.properties ├── pom.xml

Ajout des entités de base :

  • Categorie - src/main/java/com/exemple/ventes/model/Categorie.java :
package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.Data; @Entity @Data public class Categorie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nom; private String description; }
  • Produit - src/main/java/com/exemple/ventes/model/Produit.java :
package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import lombok.Data; @Entity @Data public class Produit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne private Categorie categorie; private String nom; private double prix; }
  • Client - src/main/java/com/exemple/ventes/model/Client.java :
package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.Data; @Entity @Data public class Client { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nom; private String email; private String password; private String status; // ex. "actif", "inactif" }
  • Commande - src/main/java/com/exemple/ventes/model/Commande.java :
package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import lombok.Data; import java.time.LocalDateTime; @Entity @Data public class Commande { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne private Produit produit; @ManyToOne private Client client; private LocalDateTime date; private int quantite; }
  • @Entity : Marque la classe comme une entité JPA.
  • @Id et @GeneratedValue : Définit une clé primaire auto-incrémentée.
  • @ManyToOne : Relation entre entités (ex. un produit appartient à une catégorie).
  • @Data (Lombok) : Génère getters, setters, toString, etc.

Test initial :

  1. Lancez l’application (mvn spring-boot:run).
  2. Vérifiez les logs : Hibernate crée les tables dans MySQL (gestion_ventes).

4. Développement d’API REST

4.1. Annotations REST (@RestController, @GetMapping, etc.)

Spring Boot facilite la création d’API REST avec des annotations comme @RestController pour définir un contrôleur REST et @GetMapping, @PostMapping, etc., pour mapper les méthodes HTTP aux endpoints.

graph TD A[Annotations REST] --> B(@RestController) A --> C[@GetMapping] A --> D[@PostMapping] A --> E[@PutMapping] A --> F[@DeleteMapping] B --> G[Contrôleur REST] C --> H[GET] D --> I[POST] E --> J[PUT] F --> K[DELETE]

4.2. Gestion des méthodes HTTP (GET, POST, PUT, DELETE)

Nous implémenterons un CRUD complet pour l’entité Categorie avec les méthodes HTTP correspondantes :

  • GET : Lister ou récupérer une catégorie.
  • POST : Ajouter une catégorie.
  • PUT : Mettre à jour une catégorie.
  • DELETE : Supprimer une catégorie.

Repository - src/main/java/com/exemple/ventes/repository/CategorieRepository.java :

package com.exemple.ventes.repository; import com.exemple.ventes.model.Categorie; import org.springframework.data.jpa.repository.JpaRepository; public interface CategorieRepository extends JpaRepository<Categorie, Long> { }

Contrôleur REST - src/main/java/com/exemple/ventes/controller/CategorieController.java :

package com.exemple.ventes.controller; import com.exemple.ventes.model.Categorie; import com.exemple.ventes.repository.CategorieRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/categories") public class CategorieController { @Autowired private CategorieRepository categorieRepository; // GET - Lister toutes les catégories @GetMapping public List<Categorie> listerCategories() { return categorieRepository.findAll(); } // GET - Récupérer une catégorie par ID @GetMapping("/{id}") public ResponseEntity<Categorie> obtenirCategorie(@PathVariable Long id) { return categorieRepository.findById(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } // POST - Ajouter une catégorie @PostMapping public Categorie ajouterCategorie(@RequestBody Categorie categorie) { return categorieRepository.save(categorie); } // PUT - Mettre à jour une catégorie @PutMapping("/{id}") public ResponseEntity<Categorie> mettreAJourCategorie(@PathVariable Long id, @RequestBody Categorie categorie) { if (!categorieRepository.existsById(id)) { return ResponseEntity.notFound().build(); } categorie.setId(id); Categorie updatedCategorie = categorieRepository.save(categorie); return ResponseEntity.ok(updatedCategorie); } // DELETE - Supprimer une catégorie @DeleteMapping("/{id}") public ResponseEntity<Void> supprimerCategorie(@PathVariable Long id) { if (!categorieRepository.existsById(id)) { return ResponseEntity.notFound().build(); } categorieRepository.deleteById(id); return ResponseEntity.noContent().build(); } }
  • @RequestMapping("/api/categories") : Préfixe tous les endpoints avec /api/categories.
  • @GetMapping : Liste toutes les catégories ou récupère une catégorie spécifique.
  • @PostMapping : Crée une nouvelle catégorie à partir du corps JSON.
  • @PutMapping : Met à jour une catégorie existante.
  • @DeleteMapping : Supprime une catégorie.
  • ResponseEntity : Permet de gérer les codes HTTP (200, 404, 204, etc.).
graph TD A[Méthodes HTTP] --> B(GET) A --> C(POST) A --> D(PUT) A --> E(DELETE) B --> F[Lister/Récupérer] C --> G[Ajouter] D --> H[Mettre à jour] E --> I[Supprimer]

4.3. Passage de paramètres (@PathVariable, @RequestParam, @RequestBody)

Spring Boot gère les paramètres de requête de trois façons principales :

  • @PathVariable : Extrait une valeur de l’URL (ex. /api/categories/1).
  • @RequestParam : Récupère les paramètres de requête (ex. ?nom=Electronics).
  • @RequestBody : Lie le corps JSON de la requête à un objet.

Ajout d’un endpoint avec @RequestParam dans CategorieController.java :

@GetMapping("/recherche") public List<Categorie> rechercherParNom(@RequestParam String nom) { return categorieRepository.findAll().stream() .filter(c -> c.getNom().toLowerCase().contains(nom.toLowerCase())) .toList(); }

Explication :

  • @RequestParam : Récupère le paramètre nom de l’URL (ex. /api/categories/recherche?nom=elec).
  • Filtre les catégories par nom (exemple simplifié, en production, utiliser une requête JPA).

Exemple de requêtes :

  • GET : curl http://localhost:8080/api/categories
  • GET par ID : curl http://localhost:8080/api/categories/1
  • POST : curl -X POST -H "Content-Type: application/json" -d '{"nom":"Electronics","description":"Appareils électroniques"}' http://localhost:8080/api/categories
  • PUT : curl -X PUT -H "Content-Type: application/json" -d '{"nom":"Electronics Updated","description":"Updated"}' http://localhost:8080/api/categories/1
  • DELETE : curl -X DELETE http://localhost:8080/api/categories/1
  • GET avec param : curl http://localhost:8080/api/categories/recherche?nom=elec
graph TD A[Paramètres] --> B(@PathVariable) A --> C[@RequestParam] A --> D[@RequestBody] B --> E[api/categories/id] C --> F[?nom=elec] D --> G[Corps JSON]

4.4. Réponses HTTP et codes de statut

Les API REST doivent renvoyer des codes HTTP appropriés :

  • 200 OK : Succès.
  • 201 Created : Ressource créée (optionnel pour POST).
  • 204 No Content : Suppression réussie.
  • 404 Not Found : Ressource introuvable.

Modification du POST pour retourner 201 :

@PostMapping public ResponseEntity<Categorie> ajouterCategorie(@RequestBody Categorie categorie) { Categorie savedCategorie = categorieRepository.save(categorie); return ResponseEntity.status(201).body(savedCategorie); }

Explication :

  • ResponseEntity.status(201) : Définit le code HTTP 201 pour indiquer une création réussie.
graph TD A[Réponses HTTP] --> B(ResponseEntity) B --> C[200 OK] B --> D[201 Created] B --> E[204 No Content] B --> F[404 Not Found] A --> G[Codes Statut]

Projet CRUD : Gestion des ventes - Progression

Structure actuelle :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/exemple/ventes/ │ │ │ ├── controller/ │ │ │ │ ├── TestController.java │ │ │ │ └── CategorieController.java │ │ │ ├── model/ │ │ │ │ ├── Categorie.java │ │ │ │ ├── Produit.java │ │ │ │ ├── Client.java │ │ │ │ └── Commande.java │ │ │ ├── repository/ │ │ │ │ └── CategorieRepository.java │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ └── application.properties ├── pom.xml

État actuel :

  • API REST pour Categorie implémentée avec CRUD complet :
    • GET /api/categories : Liste toutes les catégories.
    • GET /api/categories/{id} : Récupère une catégorie.
    • POST /api/categories : Ajoute une catégorie.
    • PUT /api/categories/{id} : Met à jour une catégorie.
    • DELETE /api/categories/{id} : Supprime une catégorie.
  • Les tables sont créées dans MySQL via Hibernate.

Test :

  1. Lancez l’application (mvn spring-boot:run).
  2. Testez les endpoints avec curl ou Postman (exemples ci-dessus).

Prochaines étapes :

  • Étendre le CRUD aux autres entités (Produit, Client, Commande) dans la section 5 avec Spring Data.


5. Gestion des données avec Spring Data

5.1. Introduction à Spring Data JPA

Spring Data JPA simplifie l’accès aux données en fournissant une couche d’abstraction au-dessus de JPA (Java Persistence API). Il génère automatiquement des implémentations pour les interfaces de repository, offrant des méthodes CRUD prêtes à l’emploi.

Avantages :

  • Réduction du code boilerplate.
  • Méthodes prédéfinies (findAll, save, deleteById, etc.).
  • Support des requêtes personnalisées via conventions de nommage.
graph TD A[Spring Data JPA] --> B(Abstraction JPA) B --> C[Hibernate] A --> D[Repositories] D --> E[CRUD Auto] D --> F[Requêtes Personnalisées]

5.2. Configuration d’une base de données (H2, MySQL, PostgreSQL)

Nous utilisons déjà MySQL, configuré dans application.properties. Voici un rappel :

properties
spring.datasource.url=jdbc:mysql://localhost:3306/gestion_ventes?createDatabaseIfNotExist=true spring.datasource.username=root spring.datasource.password=votre_mot_de_passe spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect

Note : Pour H2 (in-memory), il suffirait de remplacer par :

properties
spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driver-class-name=org.h2.Driver spring.jpa.hibernate.ddl-auto=create-drop
graph TD A[Configuration BD] --> B(MySQL) A --> C[H2] A --> D[PostgreSQL] B --> E[application.properties] E --> F[URL] E --> G[Dialect] E --> H[ddl-auto]

5.3. Création d’entités et repositories

Nous avons déjà les entités (Categorie, Produit, Client, Commande). Ajoutons les relations et les repositories pour toutes les entités.

Entités mises à jour :

  • Categorie.java (inchangé) :
package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import lombok.Data; @Entity @Data public class Categorie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nom; private String description; }
  • Produit.java (avec relation) :
package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import lombok.Data; @Entity @Data public class Produit { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne private Categorie categorie; private String nom; private double prix; }
  • Client.java (avec validation) :
package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.Data; @Entity @Data public class Client { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "Le nom est requis") private String nom; @Email(message = "Email invalide") private String email; @NotBlank(message = "Le mot de passe est requis") private String password; private String status; // ex. "actif", "inactif" }
  • Commande.java (avec relations) :
package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import lombok.Data; import java.time.LocalDateTime; @Entity @Data public class Commande { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne private Produit produit; @ManyToOne private Client client; private LocalDateTime date; private int quantite; }

Repositories :

  • ProduitRepository.java :
package com.exemple.ventes.repository; import com.exemple.ventes.model.Produit; import org.springframework.data.jpa.repository.JpaRepository; public interface ProduitRepository extends JpaRepository<Produit, Long> { }
  • ClientRepository.java :
package com.exemple.ventes.repository; import com.exemple.ventes.model.Client; import org.springframework.data.jpa.repository.JpaRepository; public interface ClientRepository extends JpaRepository<Client, Long> { }
  • CommandeRepository.java :
package com.exemple.ventes.repository; import com.exemple.ventes.model.Commande; import org.springframework.data.jpa.repository.JpaRepository; public interface CommandeRepository extends JpaRepository<Commande, Long> { }

Explication :

  • Les relations @ManyToOne lient Produit à Categorie et Commande à Produit/Client.
  • Les repositories héritent de JpaRepository, fournissant des méthodes CRUD.
graph TD A[Entités/Repositories] --> B(Categorie) A --> C[Produit] A --> D[Client] A --> E[Commande] B --> F[@ManyToOne] C --> F E --> F A --> G[JpaRepository] G --> H[CRUD]

5.4. Opérations CRUD avec Spring Data

Nous allons créer des contrôleurs REST pour chaque entité avec des opérations CRUD complètes.

Contrôleur Produit - src/main/java/com/exemple/ventes/controller/ProduitController.java :

package com.exemple.ventes.controller; import com.exemple.ventes.model.Produit; import com.exemple.ventes.repository.ProduitRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/produits") public class ProduitController { @Autowired private ProduitRepository produitRepository; @GetMapping public List<Produit> listerProduits() { return produitRepository.findAll(); } @GetMapping("/{id}") public ResponseEntity<Produit> obtenirProduit(@PathVariable Long id) { return produitRepository.findById(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<Produit> ajouterProduit(@RequestBody Produit produit) { Produit savedProduit = produitRepository.save(produit); return ResponseEntity.status(201).body(savedProduit); } @PutMapping("/{id}") public ResponseEntity<Produit> mettreAJourProduit(@PathVariable Long id, @RequestBody Produit produit) { if (!produitRepository.existsById(id)) { return ResponseEntity.notFound().build(); } produit.setId(id); Produit updatedProduit = produitRepository.save(produit); return ResponseEntity.ok(updatedProduit); } @DeleteMapping("/{id}") public ResponseEntity<Void> supprimerProduit(@PathVariable Long id) { if (!produitRepository.existsById(id)) { return ResponseEntity.notFound().build(); } produitRepository.deleteById(id); return ResponseEntity.noContent().build(); } }

Contrôleur Client - src/main/java/com/exemple/ventes/controller/ClientController.java :

package com.exemple.ventes.controller; import com.exemple.ventes.model.Client; import com.exemple.ventes.repository.ClientRepository; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/clients") public class ClientController { @Autowired private ClientRepository clientRepository; @GetMapping public List<Client> listerClients() { return clientRepository.findAll(); } @GetMapping("/{id}") public ResponseEntity<Client> obtenirClient(@PathVariable Long id) { return clientRepository.findById(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<Client> ajouterClient(@Valid @RequestBody Client client) { Client savedClient = clientRepository.save(client); return ResponseEntity.status(201).body(savedClient); } @PutMapping("/{id}") public ResponseEntity<Client> mettreAJourClient(@PathVariable Long id, @Valid @RequestBody Client client) { if (!clientRepository.existsById(id)) { return ResponseEntity.notFound().build(); } client.setId(id); Client updatedClient = clientRepository.save(client); return ResponseEntity.ok(updatedClient); } @DeleteMapping("/{id}") public ResponseEntity<Void> supprimerClient(@PathVariable Long id) { if (!clientRepository.existsById(id)) { return ResponseEntity.notFound().build(); } clientRepository.deleteById(id); return ResponseEntity.noContent().build(); } }

Contrôleur Commande - src/main/java/com/exemple/ventes/controller/CommandeController.java :

package com.exemple.ventes.controller; import com.exemple.ventes.model.Commande; import com.exemple.ventes.repository.CommandeRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.List; @RestController @RequestMapping("/api/commandes") public class CommandeController { @Autowired private CommandeRepository commandeRepository; @GetMapping public List<Commande> listerCommandes() { return commandeRepository.findAll(); } @GetMapping("/{id}") public ResponseEntity<Commande> obtenirCommande(@PathVariable Long id) { return commandeRepository.findById(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<Commande> ajouterCommande(@RequestBody Commande commande) { commande.setDate(LocalDateTime.now()); // Ajoute la date courante Commande savedCommande = commandeRepository.save(commande); return ResponseEntity.status(201).body(savedCommande); } @PutMapping("/{id}") public ResponseEntity<Commande> mettreAJourCommande(@PathVariable Long id, @RequestBody Commande commande) { if (!commandeRepository.existsById(id)) { return ResponseEntity.notFound().build(); } commande.setId(id); Commande updatedCommande = commandeRepository.save(commande); return ResponseEntity.ok(updatedCommande); } @DeleteMapping("/{id}") public ResponseEntity<Void> supprimerCommande(@PathVariable Long id) { if (!commandeRepository.existsById(id)) { return ResponseEntity.notFound().build(); } commandeRepository.deleteById(id); return ResponseEntity.noContent().build(); } }

Explication :

  • Chaque contrôleur utilise son repository pour les opérations CRUD.
  • @Valid sur ClientController active la validation des champs annotés dans Client.
  • LocalDateTime.now() dans CommandeController définit la date automatiquement.
graph TD A[CRUD] --> B(Lister) A --> C[Récupérer] A --> D[Ajouter] A --> E[Mettre à jour] A --> F[Supprimer] B --> G[findAll] C --> H[findById] D --> I[save] E --> I F --> J[deleteById]

Projet CRUD : Gestion des ventes - Progression

Structure actuelle :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/exemple/ventes/ │ │ │ ├── controller/ │ │ │ │ ├── TestController.java │ │ │ │ ├── CategorieController.java │ │ │ │ ├── ProduitController.java │ │ │ │ ├── ClientController.java │ │ │ │ └── CommandeController.java │ │ │ ├── model/ │ │ │ │ ├── Categorie.java │ │ │ │ ├── Produit.java │ │ │ │ ├── Client.java │ │ │ │ └── Commande.java │ │ │ ├── repository/ │ │ │ │ ├── CategorieRepository.java │ │ │ │ ├── ProduitRepository.java │ │ │ │ ├── ClientRepository.java │ │ │ │ └── CommandeRepository.java │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ └── application.properties ├── pom.xml

État actuel :

  • API REST complète pour toutes les entités :
    • /api/categories (CRUD pour Categorie).
    • /api/produits (CRUD pour Produit).
    • /api/clients (CRUD pour Client avec validation).
    • /api/commandes (CRUD pour Commande).

Test :

  1. Lancez l’application (mvn spring-boot:run).
  2. Testez avec curl ou Postman :
    • Ajouter une catégorie : curl -X POST -H "Content-Type: application/json" -d '{"nom":"Electronics","description":"Gadgets"}' http://localhost:8080/api/categories
    • Ajouter un produit : curl -X POST -H "Content-Type: application/json" -d '{"categorie":{"id":1},"nom":"TV","prix":500.0}' http://localhost:8080/api/produits
    • Ajouter un client : curl -X POST -H "Content-Type: application/json" -d '{"nom":"Alice","email":"alice@example.com","password":"1234","status":"actif"}' http://localhost:8080/api/clients
    • Ajouter une commande : curl -X POST -H "Content-Type: application/json" -d '{"produit":{"id":1},"client":{"id":1},"quantite":2}' http://localhost:8080/api/commandes

6. Vues et templates

6.1. Intégration de Thymeleaf comme moteur de templates

Thymeleaf est un moteur de templates moderne pour Spring Boot, permettant de générer des pages HTML dynamiques. Il est intégré via le starter spring-boot-starter-thymeleaf (déjà dans notre pom.xml) et fonctionne avec des fichiers HTML dans src/main/resources/templates.

Configuration existante (rappel) - application.properties :

properties
spring.thymeleaf.cache=false
  • Désactive le cache pour voir les modifications en développement sans redémarrer.
graph TD A[Thymeleaf] --> B(Starter) B --> C[spring-boot-starter-thymeleaf] A --> D[Templates] D --> E[src/main/resources/templates] A --> F[Cache] F --> G[spring.thymeleaf.cache=false]

6.2. Création de pages dynamiques

Nous allons créer des pages Thymeleaf pour lister et gérer les catégories, en réutilisant le CategorieController existant pour les données REST, mais en passant par des contrôleurs MVC pour les vues.

Contrôleur MVC - src/main/java/com/exemple/ventes/controller/CategorieWebController.java :

package com.exemple.ventes.controller; import com.exemple.ventes.model.Categorie; import com.exemple.ventes.repository.CategorieRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class CategorieWebController { @Autowired private CategorieRepository categorieRepository; @GetMapping("/categories") public String listerCategories(Model model) { model.addAttribute("categories", categorieRepository.findAll()); return "categories/liste"; } }

Vue - src/main/resources/templates/categories/liste.html :

html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Liste des Catégories</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <h1>Liste des Catégories</h1> <table> <thead> <tr> <th>ID</th> <th>Nom</th> <th>Description</th> </tr> </thead> <tbody> <tr th:each="categorie : ${categories}"> <td th:text="${categorie.id}"></td> <td th:text="${categorie.nom}"></td> <td th:text="${categorie.description}"></td> </tr> </tbody> </table> <a href="/categories/ajouter">Ajouter une catégorie</a> </body> </html>

CSS (optionnel) - src/main/resources/static/css/style.css :

css
table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: left; } th { background-color: #f2f2f2; }

Explication :

  • @Controller : Utilisé pour les vues (au lieu de @RestController).
  • Model : Transmet les données à la vue.
  • th:each : Boucle sur la liste des catégories.
  • Retourne "categories/liste" : Résout vers templates/categories/liste.html.
graph TD A[Pages Dynamiques] --> B(@Controller) B --> C[Model] A --> D[Thymeleaf] D --> E[th:each] D --> F[th:text] C --> G[Liste Categories]

6.3. Gestion des formulaires et validation

Nous allons ajouter un formulaire pour créer une catégorie, avec validation des champs.

Ajout dans CategorieWebController.java :

package com.exemple.ventes.controller; import com.exemple.ventes.model.Categorie; import com.exemple.ventes.repository.CategorieRepository; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @Controller public class CategorieWebController { @Autowired private CategorieRepository categorieRepository; @GetMapping("/categories") public String listerCategories(Model model) { model.addAttribute("categories", categorieRepository.findAll()); return "categories/liste"; } @GetMapping("/categories/ajouter") public String afficherFormulaireAjout(Model model) { model.addAttribute("categorie", new Categorie()); return "categories/ajouter"; } @PostMapping("/categories/ajouter") public String ajouterCategorie(@Valid Categorie categorie, BindingResult result, Model model) { if (result.hasErrors()) { return "categories/ajouter"; } categorieRepository.save(categorie); return "redirect:/categories"; } }

Mise à jour de Categorie.java avec validation :

package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.NotBlank; import lombok.Data; @Entity @Data public class Categorie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "Le nom est requis") private String nom; private String description; }

Vue - src/main/resources/templates/categories/ajouter.html :

html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Ajouter une Catégorie</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <h1>Ajouter une Catégorie</h1> <form th:action="@{/categories/ajouter}" th:object="${categorie}" method="post"> <div> <label>Nom :</label> <input type="text" th:field="*{nom}"> <span th:errors="*{nom}" class="error"></span> </div> <div> <label>Description :</label> <textarea th:field="*{description}"></textarea> </div> <button type="submit">Ajouter</button> </form> <a href="/categories">Retour à la liste</a> </body> <style> .error { color: red; } </style> </html>

Explication :

  • @Valid et BindingResult : Valident les champs et gèrent les erreurs.
  • th:action : Définit l’URL de soumission.
  • th:field : Lie les champs du formulaire à l’objet Categorie.
  • th:errors : Affiche les messages d’erreur.
graph TD A[Formulaires] --> B(@GetMapping) A --> C[@PostMapping] A --> D[@Valid] D --> E[BindingResult] A --> F[Thymeleaf] F --> G[th:action] F --> H[th:field] F --> I[th:errors]

6.4. Comparaison avec JSP et autres moteurs

Explication :

  • Thymeleaf : Templates HTML naturels, faciles à éditer, intégration native avec Spring Boot.
  • JSP : Plus ancien, nécessite un WAR et un serveur externe, moins flexible.
  • Freemarker : Similaire à Thymeleaf, mais syntaxe différente.

Tableau :

AspectThymeleafJSPFreemarker
SyntaxeHTML naturelTags JavaFTL
ServeurEmbarquéExterneEmbarqué
FacilitéÉlevéeMoyenneÉlevée
graph TD A[Comparaison] --> B(Thymeleaf) A --> C[JSP] A --> D[Freemarker] B --> E[HTML Naturel] C --> F[Tags Java] D --> G[FTL]

Projet CRUD : Gestion des ventes - Progression

Structure actuelle :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/exemple/ventes/ │ │ │ ├── controller/ │ │ │ │ ├── TestController.java │ │ │ │ ├── CategorieController.java │ │ │ │ ├── ProduitController.java │ │ │ │ ├── ClientController.java │ │ │ │ ├── CommandeController.java │ │ │ │ └── CategorieWebController.java │ │ │ ├── model/ │ │ │ │ ├── Categorie.java │ │ │ │ ├── Produit.java │ │ │ │ ├── Client.java │ │ │ │ └── Commande.java │ │ │ ├── repository/ │ │ │ │ ├── CategorieRepository.java │ │ │ │ ├── ProduitRepository.java │ │ │ │ ├── ClientRepository.java │ │ │ │ └── CommandeRepository.java │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ ├── static/ │ │ │ └── css/ │ │ │ └── style.css │ │ ├── templates/ │ │ │ └── categories/ │ │ │ ├── liste.html │ │ │ └── ajouter.html │ │ └── application.properties ├── pom.xml

État actuel :

  • Vues Thymeleaf pour Categorie :
    • /categories : Liste des catégories.
    • /categories/ajouter : Formulaire d’ajout avec validation.
  • API REST reste disponible pour toutes les entités.

Test :

  1. Lancez l’application (mvn spring-boot:run).
  2. Accédez à http://localhost:8080/categories.
  3. Ajoutez une catégorie via le formulaire et vérifiez la liste mise à jour.

Prochaines étapes :

  • Ajouter la sécurité avec Spring Security (section 7).

7. Sécurité avec Spring Security

7.1. Introduction à Spring Security dans Spring Boot

Spring Security est un module puissant intégré à Spring Boot pour sécuriser les applications. Il offre :

  • Authentification (vérification d’identité).
  • Autorisation (contrôle des accès).
  • Protection contre les attaques (ex. CSRF).

Nous allons sécuriser notre application pour exiger une connexion avant d’accéder aux pages et API.

graph TD A[Spring Security] --> B(Authentification) A --> C[Autorisation] A --> D[Protection] B --> E[Login] C --> F[Roles] D --> G[CSRF]

7.2. Configuration de l’authentification (in-memory, JDBC)

Nous commencerons avec une authentification in-memory simple (pour tester), puis passerons à une authentification JDBC basée sur la table Client.

Ajout de la dépendance dans pom.xml :

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>

Configuration in-memory initiale - src/main/java/com/exemple/ventes/config/SecurityConfig.java :

package com.exemple.ventes.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/css/**").permitAll() // Accès public au CSS .anyRequest().authenticated() // Tout le reste nécessite une authentification ) .formLogin(form -> form .loginPage("/login") // Page de login personnalisée .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .permitAll() ); return http.build(); } @Bean public UserDetailsService userDetailsService() { var user = User.withDefaultPasswordEncoder() .username("admin") .password("1234") .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } }

Explication :

  • @EnableWebSecurity : Active Spring Security.
  • securityFilterChain : Configure les règles d’accès.
  • UserDetailsService : Définit un utilisateur en mémoire (admin/1234).
graph TD A[Configuration] --> B(In-memory) A --> C[JDBC] B --> D[UserDetailsService] D --> E[admin/1234] A --> F[SecurityFilterChain] F --> G[Login/Logout]

7.3. Sécurisation des endpoints REST et des pages

Nous allons sécuriser les pages Thymeleaf et les API REST, tout en ajoutant une page de login.

Page de login - src/main/resources/templates/login.html :

html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Connexion</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <h1>Connexion</h1> <div th:if="${param.error}" class="error">Nom d'utilisateur ou mot de passe incorrect.</div> <div th:if="${param.logout}" class="success">Déconnexion réussie.</div> <form th:action="@{/login}" method="post"> <div> <label>Nom d'utilisateur :</label> <input type="text" name="username"> </div> <div> <label>Mot de passe :</label> <input type="password" name="password"> </div> <button type="submit">Se connecter</button> </form> </body> <style> .error { color: red; } .success { color: green; } </style> </html>

Mise à jour de CategorieWebController.java : Ajoutez un lien de déconnexion :

@GetMapping("/categories") public String listerCategories(Model model) { model.addAttribute("categories", categorieRepository.findAll()); return "categories/liste"; }

Mise à jour de liste.html :

html
<a href="/logout">Déconnexion</a> <!-- Ajouter après "Ajouter une catégorie" -->

Sécurisation des API REST : Modifiez SecurityConfig.java pour limiter les API aux utilisateurs authentifiés :

http .authorizeHttpRequests(auth -> auth .requestMatchers("/css/**", "/login").permitAll() .requestMatchers("/api/**").authenticated() // Sécurise toutes les API .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .permitAll() );
graph TD A[Sécurisation] --> B(Pages) A --> C[API REST] B --> D[categories] C --> E[api/**] A --> F[Login] F --> G[login]

7.4. Gestion des rôles et permissions (JDBC)

Passons à une authentification basée sur la table Client avec email/password comme identifiants.

Mise à jour de Client.java : Ajoutez un rôle :

package com.exemple.ventes.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.Data; @Entity @Data public class Client { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank(message = "Le nom est requis") private String nom; @Email(message = "Email invalide") private String email; @NotBlank(message = "Le mot de passe est requis") private String password; private String status; private String role = "USER"; // Par défaut, tous sont USER }

Service de sécurité - src/main/java/com/exemple/ventes/service/ClientUserDetailsService.java :

package com.exemple.ventes.service; import com.exemple.ventes.model.Client; import com.exemple.ventes.repository.ClientRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; 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 ClientUserDetailsService implements UserDetailsService { @Autowired private ClientRepository clientRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { Client client = clientRepository.findByEmail(email) .orElseThrow(() -> new UsernameNotFoundException("Client non trouvé : " + email)); return User.withUsername(client.getEmail()) .password(client.getPassword()) // Doit être encodé en production .roles(client.getRole()) .build(); } }

Ajout dans ClientRepository.java :

import java.util.Optional; public interface ClientRepository extends JpaRepository<Client, Long> { Optional<Client> findByEmail(String email); }

Mise à jour de SecurityConfig.java pour JDBC :

package com.exemple.ventes.config; import com.exemple.ventes.service.ClientUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private ClientUserDetailsService clientUserDetailsService; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/css/**", "/login").permitAll() .requestMatchers("/api/**").authenticated() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .permitAll() ); return http.build(); } @Bean public UserDetailsService userDetailsService() { return clientUserDetailsService; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

Mise à jour de ClientController.java pour encoder le mot de passe :

@PostMapping public ResponseEntity<Client> ajouterClient(@Valid @RequestBody Client client) { client.setPassword(passwordEncoder.encode(client.getPassword())); Client savedClient = clientRepository.save(client); return ResponseEntity.status(201).body(savedClient); } @PutMapping("/{id}") public ResponseEntity<Client> mettreAJourClient(@PathVariable Long id, @Valid @RequestBody Client client) { if (!clientRepository.existsById(id)) { return ResponseEntity.notFound().build(); } client.setId(id); client.setPassword(passwordEncoder.encode(client.getPassword())); Client updatedClient = clientRepository.save(client); return ResponseEntity.ok(updatedClient); } @Autowired private PasswordEncoder passwordEncoder; // Ajouter au contrôleur

Explication :

  • ClientUserDetailsService : Charge les utilisateurs depuis la table Client.
  • BCryptPasswordEncoder : Encode les mots de passe pour la sécurité.
  • Les clients doivent maintenant être ajoutés via l’API avec un mot de passe clair, qui sera encodé.

Test :

  1. Ajoutez un client via l’API : curl -X POST -H "Content-Type: application/json" -d '{"nom":"Bob","email":"bob@example.com","password":"1234","status":"actif"}' http://localhost:8080/api/clients
  2. Connectez-vous avec bob@example.com / 1234 sur /login.
graph TD A[Roles/Permissions] --> B(JDBC) B --> C[Client] C --> D[findByEmail] A --> E[PasswordEncoder] E --> F[BCrypt]

Projet CRUD : Gestion des ventes - Progression

Structure actuelle :

gestion-ventes/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/exemple/ventes/ │ │ │ ├── config/ │ │ │ │ └── SecurityConfig.java │ │ │ ├── controller/ │ │ │ │ ├── TestController.java │ │ │ │ ├── CategorieController.java │ │ │ │ ├── ProduitController.java │ │ │ │ ├── ClientController.java │ │ │ │ ├── CommandeController.java │ │ │ │ └── CategorieWebController.java │ │ │ ├── model/ │ │ │ │ ├── Categorie.java │ │ │ │ ├── Produit.java │ │ │ │ ├── Client.java │ │ │ │ └── Commande.java │ │ │ ├── repository/ │ │ │ │ ├── CategorieRepository.java │ │ │ │ ├── ProduitRepository.java │ │ │ │ ├── ClientRepository.java │ │ │ │ └── CommandeRepository.java │ │ │ ├── service/ │ │ │ │ └── ClientUserDetailsService.java │ │ │ └── GestionVentesApplication.java │ │ └── resources/ │ │ ├── static/ │ │ │ └── css/ │ │ │ └── style.css │ │ ├── templates/ │ │ │ ├── categories/ │ │ │ │ ├── liste.html │ │ │ │ └── ajouter.html │ │ │ └── login.html │ │ └── application.properties ├── pom.xml

État actuel :

  • Application sécurisée avec Spring Security :
    • Authentification basée sur Client (email/password).
    • Pages (/categories) et API (/api/**) protégées.
    • Page de login personnalisée.

Test :

  1. Lancez l’application (mvn spring-boot:run).
  2. Accédez à http://localhost:8080/categories : redirection vers /login.
  3. Connectez-vous avec un client existant (ex. bob@example.com / 1234).

Prochaines étapes :

  • Ajouter des fonctionnalités avancées (section 8).