Gestion des utilisateurs
Notions théoriques
L'entité Utilisateur pour l'authentification
L'entité Utilisateur stocke les informations d'un compte : email (identifiant unique), mot de passe haché, et rôle. Le mot de passe ne doit jamais être stocké en clair.
@Entity
@Table(name = "utilisateurs")
public class Utilisateur {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Email
@Column(unique = true, nullable = false, length = 180)
private String email;
@Column(nullable = false)
private String password; // Hashé avec BCrypt, jamais en clair
@Column(length = 50)
private String role; // "ROLE_USER" ou "ROLE_ADMIN"
// getters/setters...
}
Stocker des mots de passe en clair est une faille de sécurité critique. Si la base de données est compromise, tous les mots de passe sont exposés. Utilisez toujours BCrypt (ou Argon2) pour hacher les mots de passe. Le hash BCrypt inclut un "sel" aléatoire qui protège contre les attaques par table arc-en-ciel.
PasswordEncoder : hacher et vérifier les mots de passe
Spring Security fournit l'interface PasswordEncoder. L'implémentation recommandée est BCryptPasswordEncoder :
// Bean à déclarer dans une classe @Configuration
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Utilisation :
// Hacher un mot de passe avant de l'enregistrer
String motDePasseHache = passwordEncoder.encode("monMotDePasse123");
utilisateur.setPassword(motDePasseHache);
// Vérifier qu'un mot de passe correspond au hash
boolean correspond = passwordEncoder.matches("motDePasseSaisi", utilisateur.getPassword());
BCryptPasswordEncoder.encode() génère un hash différent à chaque appel (grâce au sel aléatoire), mais matches() peut toujours vérifier si le mot de passe en clair correspond au hash.
UserDetailsService : lier Utilisateur à Spring Security
Pour que Spring Security sache comment charger un utilisateur depuis la base de données, il faut implémenter l'interface UserDetailsService :
package org.joliciel.monblog.service;
import org.joliciel.monblog.entity.Utilisateur;
import org.joliciel.monblog.repository.UtilisateurRepository;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UtilisateurService implements UserDetailsService {
private final UtilisateurRepository utilisateurRepository;
public UtilisateurService(UtilisateurRepository utilisateurRepository) {
this.utilisateurRepository = utilisateurRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Utilisateur utilisateur = utilisateurRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé : " + email));
return new org.springframework.security.core.userdetails.User(
utilisateur.getEmail(),
utilisateur.getPassword(),
List.of(new SimpleGrantedAuthority(utilisateur.getRole()))
);
}
}
@Service: Spring détecte et enregistre ce service automatiquementloadUserByUsername(email): Spring Security appelle cette méthode avec l'email saisi dans le formulaire de connexionSimpleGrantedAuthority: représente un rôle ou une permission (ex :"ROLE_USER")
Inscription : créer un compte utilisateur
@Controller
public class InscriptionController {
private final UtilisateurRepository utilisateurRepository;
private final PasswordEncoder passwordEncoder;
public InscriptionController(UtilisateurRepository utilisateurRepository,
PasswordEncoder passwordEncoder) {
this.utilisateurRepository = utilisateurRepository;
this.passwordEncoder = passwordEncoder;
}
@GetMapping("/inscription")
public String afficherFormulaire(Model model) {
model.addAttribute("utilisateur", new Utilisateur());
return "auth/inscription";
}
@PostMapping("/inscription")
public String inscrire(@Valid @ModelAttribute Utilisateur utilisateur,
BindingResult result,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
return "auth/inscription";
}
// Hacher le mot de passe avant l'enregistrement
utilisateur.setPassword(passwordEncoder.encode(utilisateur.getPassword()));
utilisateur.setRole("ROLE_USER");
utilisateurRepository.save(utilisateur);
redirectAttributes.addFlashAttribute("message", "Compte créé ! Vous pouvez vous connecter.");
return "redirect:/connexion";
}
}
Repository Utilisateur avec recherche par email
public interface UtilisateurRepository extends JpaRepository<Utilisateur, Long> {
Optional<Utilisateur> findByEmail(String email);
}
Exemple pratique
Configuration Spring Security minimale pour activer l'authentification (nécessaire pour utiliser PasswordEncoder comme bean) :
package org.joliciel.monblog.config;
import org.joliciel.monblog.service.UtilisateurService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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 {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider(UtilisateurService utilisateurService) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(utilisateurService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/inscription", "/connexion").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/connexion")
.defaultSuccessUrl("/articles", true)
.permitAll()
)
.logout(logout -> logout.logoutSuccessUrl("/connexion"));
return http.build();
}
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Dans ce TP, vous allez créer le formulaire d'inscription fonctionnel pour MonBlog.
Étape 1 — Créer UtilisateurRepository avec findByEmail
Retournez Optional<Utilisateur> plutôt que Utilisateur (qui serait null si non trouvé). Cela force le code appelant à traiter explicitement le cas "utilisateur non trouvé" avec .orElseThrow() ou .isPresent(), évitant les NullPointerException.
Étape 2 — Implémenter UserDetailsService
Fournissez un message d'erreur dans UsernameNotFoundException. Ce message apparaît dans les logs serveur et aide au débogage. En revanche, ne l'affichez jamais directement à l'utilisateur (cela révèlerait si un email est enregistré ou non). Affichez toujours "Identifiants incorrects" à l'utilisateur, sans préciser si c'est l'email ou le mot de passe qui est faux.
Étape 3 — Hacher le mot de passe dans le contrôleur d'inscription
Le hachage doit se faire juste avant save(), jamais avant. Si vous hachez le mot de passe dès la réception du formulaire et qu'une erreur de validation se produit ensuite (email déjà utilisé par exemple), vous risquez de stocker un hash d'un hash si le formulaire est resoumis. Hachez uniquement quand toutes les validations sont passées.