Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 74

Authentification basique par utilisateur et mot de passe

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons fait un grand pas dans la sécurisation de notre API en configurant Spring Security avec une authentification basique et des utilisateurs stockés en mémoire. C'était un excellent point de départ pour comprendre les concepts fondamentaux.

Aujourd'hui, nous allons rendre notre système d'authentification bien plus robuste et professionnel, ce qui est indispensable pour une application réelle. Nous allons remplacer notre gestionnaire d'utilisateurs en mémoire par un système persistant, en stockant les informations d'identification dans notre base de données MySQL. L'objectif est de mettre en place une authentification par nom d'utilisateur et mot de passe qui s'appuie sur des données réelles et persistantes, une compétence clé pour tout développeur backend.

Pour ce faire, nous allons :

  1. Créer une entité JPA User pour modéliser nos utilisateurs.
  2. Implémenter un UserRepository pour interagir avec la base de données.
  3. Développer notre propre UserDetailsService pour que Spring Security puisse charger les utilisateurs depuis la base.
  4. Mettre en place un endpoint pour permettre l'inscription de nouveaux utilisateurs, en veillant à hacher correctement leurs mots de passe.

1. D'une gestion en mémoire à une base de données

Dans la leçon précédente, nous avons utilisé InMemoryUserDetailsManager. C'est très pratique pour des tests rapides ou des démonstrations, mais cela présente des inconvénients majeurs pour une application de production :

  • Volatilité : Toutes les informations sur les utilisateurs sont perdues à chaque redémarrage de l'application.
  • Gestion limitée : Il est difficile de gérer un grand nombre d'utilisateurs ou de permettre leur enregistrement dynamique.
  • Non évolutif : Ne convient pas à une architecture distribuée où plusieurs instances de l'application pourraient s'exécuter.

La solution professionnelle consiste à stocker les utilisateurs dans une base de données. Cela nous permet de gérer les utilisateurs de manière persistante, de les créer, de les mettre à jour et de les supprimer via notre API.


2. Les acteurs de l'authentification basée sur la base de données

Le flux d'authentification que nous allons construire implique plusieurs composants clés de Spring Security. La vidéo ci-dessous offre une excellente explication conceptuelle de ces acteurs et de leurs interactions.

Sécuriser une application Spring Boot 3 avec Spring Security 6 | Authentification et Autorisations

La vidéo "Sécuriser une application Spring Boot 3" de la chaîne Autour Du Code détaille l'architecture de Spring Security.

Regardez la section expliquant les composants principaux. Portez une attention particulière aux rôles de : AuthenticationManager : L'orchestrateur central de l'authentification. UserDetailsService : L'interface chargée de trouver un utilisateur par son nom. C'est ce que nous allons implémenter. PasswordEncoder : L'outil indispensable pour hacher et vérifier les mots de passe.

Voici un diagramme de la documentation officielle de Spring qui illustre comment un filtre intercepte une requête et délègue l'authentification au AuthenticationManager.

Ce diagramme illustre le processus d'authentification dans Spring Security. Le `BasicAuthenticationFilter` extrait les identifiants, crée un token et le passe à l'`AuthenticationManager` pour validation.

Maintenant que la théorie est posée, passons à la pratique. La vidéo suivante sera notre guide principal pour l'implémentation.


3. Implémentation de l'authentification avec JPA

Nous allons suivre une démarche structurée pour remplacer notre InMemoryUserDetailsManager.

Étape 1 : L'entité User et son Repository

La première étape consiste à créer une table dans notre base de données pour stocker les utilisateurs. Avec JPA, cela se traduit par la création d'une classe User annotée avec @Entity.

Login et Inscription avec Spring Boot | Spring Security | MySQL

La vidéo "Login et Inscription avec Spring Boot" de Coding With Armand montre comment créer l'entité et le repository.

Regardez la partie sur la création de l'entité. L'auteur y définit la classe User et son UserRepository.

Voici le code correspondant que vous pouvez ajouter à votre projet. Créez un package model ou entity si ce n'est pas déjà fait.

User.java

package com.example.ecommerce.model;

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

@Data // Annotation de Lombok pour générer getters, setters, toString, etc.
@Entity
@Table(name = "users") // "user" est souvent un mot-clé réservé en SQL
public class User {

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

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String role; // Par simplicité, nous stockons le rôle comme une chaîne.
}

Ensuite, créez l'interface UserRepository dans votre package repository.

UserRepository.java

package com.example.ecommerce.repository;

import com.example.ecommerce.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    // Méthode pour trouver un utilisateur par son nom d'utilisateur.
    // Spring Data JPA génère l'implémentation automatiquement.
    Optional<User> findByUsername(String username);
}

Notez que nous utilisons Optional<User> pour gérer proprement le cas où l'utilisateur n'est pas trouvé.

Étape 2 : Le CustomUserDetailsService

Maintenant, nous devons créer une classe qui implémente l'interface UserDetailsService de Spring Security. C'est le pont entre Spring Security et notre UserRepository.

Login et Inscription avec Spring Boot | Spring Security | MySQL

La vidéo continue en montrant comment implémenter le service qui chargera les utilisateurs.

Suivez attentivement la création du CustomUserDetailsService. C'est ici que la logique de récupération de l'utilisateur depuis la base de données est mise en place.

Créez un package service et ajoutez la classe suivante.

CustomUserDetailsService.java

package com.example.ecommerce.service;

import com.example.ecommerce.model.User;
import com.example.ecommerce.repository.UserRepository;
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 java.util.Collections;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. Chercher l'utilisateur dans la base de données
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé avec le nom : " + username));

        // 2. Transformer notre entité User en un UserDetails de Spring Security
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole())) // Spring Security ajoute le préfixe "ROLE_"
        );
    }
}

Cette classe est cruciale. Elle prend un username, utilise le UserRepository pour interroger la base de données, et si l'utilisateur est trouvé, elle le convertit en un objet UserDetails que Spring Security peut comprendre, y compris son rôle.

Étape 3 : Mise à jour de la configuration de sécurité

Notre ancienne configuration utilisait InMemoryUserDetailsManager. Nous devons la mettre à jour pour qu'elle utilise nos nouveaux composants. La vidéo explique comment configurer un AuthenticationManager qui va utiliser notre CustomUserDetailsService et notre PasswordEncoder.

Login et Inscription avec Spring Boot | Spring Security | MySQL

La configuration de la sécurité est l'étape qui relie tous les éléments.

Regardez comment la classe SecurityConfig est construite. Concentrez-vous sur la création des beans SecurityFilterChain et AuthenticationManager. Ce dernier est nouveau et très important.

Modifiez votre SecurityConfig pour retirer le UserDetailsService en mémoire et ajouter le AuthenticationManager.

SecurityConfig.java

package com.example.ecommerce.config;

import com.example.ecommerce.service.CustomUserDetailsService;
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.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfig {

    private final CustomUserDetailsService customUserDetailsService;

    public SecurityConfig(CustomUserDetailsService customUserDetailsService) {
        this.customUserDetailsService = customUserDetailsService;
    }

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

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
        return authenticationManagerBuilder.build();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // Endpoints pour l'inscription/connexion
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());

        return http.build();
    }
}

Points clés de la mise à jour :

  • Nous n'avons plus de bean UserDetailsService pour les utilisateurs en mémoire.
  • Nous injectons notre CustomUserDetailsService.
  • Nous créons un bean AuthenticationManager qui est configuré pour utiliser notre customUserDetailsService et le BCryptPasswordEncoder. C'est lui qui gérera les tentatives de connexion.
  • Nous autorisons l'accès public à /api/auth/**, où nous placerons nos endpoints de connexion et d'inscription.

4. Inscription et Connexion

Un système d'authentification n'est pas complet si on ne peut pas y ajouter de nouveaux utilisateurs. Créons un RestController pour gérer cela.

Login et Inscription avec Spring Boot | Spring Security | MySQL

La fin de la vidéo montre comment créer les endpoints pour l'inscription et la connexion.

Regardez d'abord comment créer l'endpoint d'inscription. L'étape la plus importante est le hachage du mot de passe avant de sauvegarder l'utilisateur. Ensuite, voyez l'endpoint de connexion, qui utilise AuthenticationManager pour vérifier les identifiants.

Créez un AuthController dans un package controller.

AuthController.java

package com.example.ecommerce.controller;

import com.example.ecommerce.model.User;
import com.example.ecommerce.repository.UserRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.authenticationManager = authenticationManager;
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @PostMapping("/register")
    public ResponseEntity<String> registerUser(@RequestBody User user) {
        if (userRepository.findByUsername(user.getUsername()).isPresent()) {
            return new ResponseEntity<>("Le nom d'utilisateur est déjà pris !", HttpStatus.BAD_REQUEST);
        }

        // Hacher le mot de passe avant de le sauvegarder
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        
        userRepository.save(user);

        return new ResponseEntity<>("Utilisateur enregistré avec succès !", HttpStatus.CREATED);
    }
    
    @PostMapping("/login")
    public ResponseEntity<String> loginUser(@RequestBody User user) {
        try {
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())
            );
            SecurityContextHolder.getContext().setAuthentication(authentication);
            return new ResponseEntity<>("Connexion réussie !", HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>("Identifiants invalides.", HttpStatus.UNAUTHORIZED);
        }
    }
}

Attention : La méthode registerUser est cruciale. Elle s'assure de bien hacher le mot de passe avec passwordEncoder.encode() avant de le sauvegarder. Ne jamais stocker de mot de passe en clair !


Conclusion

Félicitations ! Vous avez transformé le système d'authentification de votre application, le faisant passer d'un simple prototype en mémoire à une solution robuste et persistante digne d'une application professionnelle.

Points clés à retenir :

  • Le stockage des utilisateurs en base de données est la norme en production.
  • Vous devez implémenter votre propre UserDetailsService pour indiquer à Spring Security comment charger les utilisateurs depuis votre source de données (ici, une base de données via JPA).
  • AuthenticationManager est le bean qui orchestre le processus d'authentification en utilisant votre UserDetailsService et votre PasswordEncoder.
  • Lors de l'enregistrement d'un nouvel utilisateur, il est impératif de hacher le mot de passe avant de le stocker.

Maintenant que nous avons des utilisateurs avec des rôles (USER, ADMIN, etc.) stockés de manière persistante, nous sommes prêts pour la prochaine étape logique : l'autorisation. Dans la prochaine leçon, nous apprendrons à utiliser ces rôles pour contrôler finement qui peut accéder à quels endpoints. Par exemple, autoriser tout le monde à voir les produits, mais réserver la création de nouveaux produits aux seuls administrateurs.