Aller au contenu principal

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...
}
Ne jamais stocker un mot de passe en clair

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 automatiquement
  • loadUserByUsername(email) : Spring Security appelle cette méthode avec l'email saisi dans le formulaire de connexion
  • SimpleGrantedAuthority : 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


Quelle interface Spring Security faut-il implémenter pour charger un utilisateur depuis la base de données ?


Que retourne la méthode loadUserByUsername() ?


Pourquoi BCryptPasswordEncoder génère-t-il un hash différent à chaque appel de encode() ?


Quelle méthode du PasswordEncoder vérifie qu'un mot de passe en clair correspond à un hash ?


Quel est le format conventionnel des rôles Spring Security ?


Pourquoi ne faut-il pas stocker le mot de passe en clair dans la base de données ?


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


Bonne pratique - Optional pour 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


Bonne pratique - Message d'erreur dans UsernameNotFoundException

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


Bonne pratique - Toujours hacher AVANT la sauvegarde

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.

📌 Une solution