Contactez-nous

Authentification basée sur une base de données (JDBC/JPA)

Implémentez l'authentification basée sur base de données dans Spring Boot en utilisant UserDetailsService, PasswordEncoder et Spring Data JPA/JDBC.

Le scénario le plus courant : Vos utilisateurs, votre base

Si l'utilisateur en mémoire fourni par défaut est pratique pour démarrer, la quasi-totalité des applications réelles stockent les informations de leurs utilisateurs (identifiants, mots de passe, rôles, etc.) dans leur propre base de données. C'est le mécanisme d'authentification le plus courant et le plus flexible, car il vous donne un contrôle total sur la structure et la gestion des données utilisateur.

Spring Security s'intègre parfaitement à ce scénario. Il fournit les interfaces et les points d'extension nécessaires pour que vous puissiez lui indiquer comment récupérer les détails d'un utilisateur depuis votre base de données (via JDBC ou JPA) afin de vérifier ses informations d'identification lors d'une tentative de connexion.

Les deux piliers de cette intégration sont l'interface UserDetailsService et l'interface PasswordEncoder.

Le coeur de la recherche : `UserDetailsService`

L'interface org.springframework.security.core.userdetails.UserDetailsService est au centre de l'authentification basée sur les données. Elle définit une seule méthode cruciale :

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

Votre responsabilité est de fournir une implémentation de cette interface (généralement sous forme d'un bean Spring, par exemple une classe @Service). Lorsque Spring Security doit authentifier un utilisateur (par exemple, après la soumission d'un formulaire de connexion), il appelle la méthode loadUserByUsername de votre bean en lui passant le nom d'utilisateur saisi.

Votre implémentation doit alors :

  1. Rechercher l'utilisateur dans votre base de données en utilisant le nom d'utilisateur fourni.
  2. Si l'utilisateur n'est pas trouvé, elle doit lever une exception UsernameNotFoundException (c'est important pour la sécurité, ne retournez pas null).
  3. Si l'utilisateur est trouvé, elle doit créer et retourner un objet qui implémente l'interface org.springframework.security.core.userdetails.UserDetails.

Décrire l'utilisateur : `UserDetails` et `GrantedAuthority`

L'objet UserDetails retourné par loadUserByUsername est un contrat qui fournit à Spring Security les informations essentielles sur l'utilisateur trouvé dans la base de données :

  • getUsername(): Le nom d'utilisateur (doit correspondre à celui recherché).
  • getPassword(): Le mot de passe haché stocké dans la base de données (jamais le mot de passe en clair !).
  • isEnabled(): Un booléen indiquant si le compte utilisateur est actif.
  • isAccountNonExpired(): Indique si le compte a expiré.
  • isCredentialsNonExpired(): Indique si les informations d'identification (mot de passe) ont expiré.
  • isAccountNonLocked(): Indique si le compte est verrouillé.
  • getAuthorities(): Une collection de GrantedAuthority représentant les rôles ou permissions accordés à l'utilisateur (par exemple, "ROLE_USER", "ROLE_ADMIN", "READ_PRIVILEGE").

Spring Security fournit une implémentation concrète pratique, org.springframework.security.core.userdetails.User, que vous pouvez utiliser directement. Vous pouvez aussi créer votre propre classe qui implémente UserDetails, souvent en encapsulant votre entité JPA ou votre objet métier utilisateur.

Les GrantedAuthority sont typiquement créées à partir des rôles ou permissions stockés dans votre base (par exemple, en utilisant SimpleGrantedAuthority).

La sécurité des mots de passe : `PasswordEncoder`

Il est absolument impératif de ne JAMAIS stocker les mots de passe des utilisateurs en clair dans votre base de données. En cas de fuite de données, tous les mots de passe seraient compromis. Vous devez toujours stocker une version hachée et salée du mot de passe.

Spring Security fournit l'interface org.springframework.security.crypto.password.PasswordEncoder pour gérer cela de manière sécurisée. Son rôle est double :

  1. Encoder (hacher) : Prendre un mot de passe en clair fourni lors de l'inscription ou de la modification et générer une chaîne hachée sécurisée à stocker dans la base.String encode(CharSequence rawPassword);
  2. Vérifier : Prendre un mot de passe en clair soumis lors de la connexion et le comparer à la version hachée stockée dans la base.boolean matches(CharSequence rawPassword, String encodedPassword);

L'implémentation fortement recommandée est BCryptPasswordEncoder, qui utilise l'algorithme de hachage robuste et adaptatif BCrypt, intégrant automatiquement le salage.

Vous devez déclarer un bean PasswordEncoder dans votre configuration Spring pour que Spring Security puisse l'utiliser.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // Utilise BCrypt avec une force par défaut (actuellement 10)
        return new BCryptPasswordEncoder(); 
        // Vous pouvez spécifier une force : new BCryptPasswordEncoder(12)
    }
    
    // ... autres beans de configuration (SecurityFilterChain, etc.)
}

Lors de l'inscription, vous utiliserez ce bean pour encoder le mot de passe avant de le sauvegarder : String encodedPassword = passwordEncoder.encode(rawPassword);. Lors de la connexion, Spring Security utilisera automatiquement ce même bean pour appeler matches(submittedPassword, hashedPasswordFromDB).

Exemple avec Spring Data JPA

1. Entités JPA (Utilisateur et Rôle) :

@Entity
@Table(name = "app_user")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true, nullable = false)
    private String username;
    @Column(nullable = false)
    private String password; // Stocke le mot de passe haché
    private boolean enabled = true;

    @ManyToMany(fetch = FetchType.EAGER) // Charger les rôles avec l'utilisateur
    @JoinTable(
        name = "user_roles", 
        joinColumns = @JoinColumn(name = "user_id"), 
        inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set roles = new HashSet<>();
    // Getters, Setters...
}

@Entity
@Table(name = "app_role")
public class Role {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(unique = true, nullable = false)
    private String name; // Ex: "ROLE_USER", "ROLE_ADMIN"
    // Getters, Setters...
}

2. Repository JPA :

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository {
    Optional findByUsername(String username);
}

3. Implémentation de `UserDetailsService` :

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class JpaUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public JpaUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé: " + username));

        // Convertit les rôles de l'entité en GrantedAuthority pour Spring Security
        Collection authorities = 
            user.getRoles().stream()
                .map(role -> new SimpleGrantedAuthority(role.getName()))
                .collect(Collectors.toSet());

        // Retourne un objet UserDetails (ici l'implémentation fournie par Spring Security)
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(), 
                user.getPassword(), // Le mot de passe HACHE de la BDD
                user.isEnabled(), 
                true, // accountNonExpired
                true, // credentialsNonExpired
                true, // accountNonLocked
                authorities);
    }
}

4. Configuration de Spring Security (`SecurityFilterChain`) :

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 {

    // Injection de notre UserDetailsService personnalisé
    // private final JpaUserDetailsService jpaUserDetailsService;
    // public SecurityConfig(JpaUserDetailsService jpaUserDetailsService) {...
    
    @Bean // Bean PasswordEncoder (vu précédemment)
    public PasswordEncoder passwordEncoder() { 
        return new BCryptPasswordEncoder(); 
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                // Définir ici les règles d'autorisation (voir chapitre suivant)
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/", "/home", "/css/**", "/js/**").permitAll() // Exemples d'URLs publiques
                .anyRequest().authenticated() // Toutes les autres requêtes nécessitent authentification
            )
            .formLogin(form -> form // Configuration du formulaire de connexion
                .loginPage("/login") // URL de la page de login personnalisée (optionnel)
                .permitAll() // Permettre l'accès à la page de login
                .defaultSuccessUrl("/welcome", true) // Page après succès login
                // .failureUrl("/login?error") // Page en cas d'échec
            )
            .logout(logout -> logout // Configuration de la déconnexion
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            ); 
            // Spring utilisera automatiquement le JpaUserDetailsService et le PasswordEncoder
            // car ils sont disponibles en tant que beans dans le contexte.
            // Configuration explicite (moins courante avec Boot si les beans existent) :
            // http.userDetailsService(jpaUserDetailsService);
            
        return http.build();
    }
}

Avec cette configuration, lorsque l'utilisateur soumet le formulaire de connexion, Spring Security utilisera votre JpaUserDetailsService pour récupérer les détails de l'utilisateur depuis la base de données JPA et le BCryptPasswordEncoder pour vérifier le mot de passe.

Alternative avec JDBC

L'approche avec JDBC est très similaire. La principale différence réside dans l'implémentation du UserDetailsService. Au lieu d'utiliser un UserRepository JPA, vous utiliseriez JdbcTemplate ou un repository Spring Data JDBC pour exécuter les requêtes SQL nécessaires afin de récupérer les informations de l'utilisateur et ses rôles/autorités depuis la base de données.

Spring Security fournit également une implémentation prête à l'emploi, JdbcDaoImpl, que vous pouvez configurer si votre schéma de base de données respecte la structure attendue par cette classe (tables `users` et `authorities`). Cependant, implémenter votre propre UserDetailsService avec JdbcTemplate offre plus de flexibilité.

Conclusion : Une authentification personnalisée et sécurisée

L'authentification basée sur une base de données est la méthode la plus flexible et la plus couramment utilisée pour gérer les utilisateurs dans Spring Security. En implémentant l'interface UserDetailsService pour récupérer les informations depuis votre base (via JPA ou JDBC) et en configurant un PasswordEncoder robuste (comme BCrypt), vous pouvez mettre en place un système d'authentification personnalisé, sécurisé et adapté aux besoins spécifiques de votre application Spring Boot.