CodeGym /Cours /Spring Security /Utilisateurs en mémoire

Utilisateurs en mémoire

Spring Security
Niveau 7, Leçon 0
Disponible

1. Introduction

Si l'on considère le projet comme une petite ville, alors SecurityFilterChain est comme les panneaux de signalisation et les points de contrôle : « par ici tout le monde peut passer », « par ici seulement sur autorisation », « par ici seulement les éditeurs », « et par ici seulement les admins ». Mais si vous n'avez pas de « citoyens » (comptes utilisateur) en tant que tels, vous ne pouvez pas vérifier correctement que ces règles fonctionnent, et le plus ennuyeux, vous ne pouvez pas reproduire la même vérification une semaine plus tard.

Jusqu'à présent, Spring Boot pouvait créer un utilisateur par défaut (avec un mot de passe aléatoire) pour démontrer le principe secure-by-default. Mais dès que vous commencez à construire une matrice d'accès pour Secure Content Platform API, vous avez besoin de comptes scénarisés : un utilisateur normal, un éditeur, un administrateur. Et de tels comptes dont vous connaissez exactement les identifiants et mots de passe, sans être surpris de voir un mot de passe différent chaque jour, et capables d'obtenir la même configuration à chaque démarrage.

C'est pourquoi le premier modèle d'utilisateurs significatif dans ce cours doit être le plus simple possible et le plus contrôlable possible : in-memory.

2. In-memory plutôt que base de données au début

Il y a une idée reçue très répandue : « si on fait un backend, il faut tout de suite une base de données ». En production réelle, oui, les utilisateurs vivent presque toujours dans une base de données ou un fournisseur d'identité externe. Mais dans l'apprentissage, ce qui importe est de comprendre le mécanisme de Spring Security sans qu'il soit noyé dans des détails annexes.

Une base de données apporte un énorme « bruit » : migrations, SQL, mappings JPA, transactions, lazy-loading, conteneurs de test, données d'amorçage, conflits d'unicité, environnements… Et tout cela éclipse immédiatement la question principale du jour : comment Spring Security récupère un utilisateur par son nom d'utilisateur et comment nous gérons l'ensemble des utilisateurs.

Le modèle in-memory est un compromis pédagogique conscient. Nous « gelons » temporairement la complexité du stockage externe pour nous concentrer sur la pure mécanique de sécurité. Nous devons voir que :

1) Spring Security a un endroit où il « demande » : « donne-moi l'utilisateur par son nom » ;

2) nous pouvons remplacer la source par défaut des utilisateurs par la nôtre ;

3) le mot de passe doit toujours être encodé, même si l'utilisateur ne vit qu'en mémoire.

Et nous aborderons la base de données plus tard — avec un modèle mental correctement assemblé, pour ne pas transformer tout le cours en « pourquoi j'ai un NullPointerException dans Hibernate quand j'essaie de me connecter ».

3. Place de InMemoryUserDetailsManager dans la chaîne

Il est maintenant utile de revenir un instant sur l'architecture d'authentification que nous avons déjà abordée dans le module 1, mais de manière très appliquée. Spring Security, dans le modèle classique username/password (celui qui nous intéresse), effectue approximativement la chaîne d'actions suivante : les filtres reçoivent la requête, puis l'authentification est lancée, ensuite AuthenticationManager délègue la vérification à AuthenticationProvider, qui s'adresse à UserDetailsService, et seulement ensuite a lieu la vérification du mot de passe via PasswordEncoder.

InMemoryUserDetailsManager est précisément le composant qui, dans cette chaîne, joue le rôle de UserDetailsService (et même plus). Il est capable de renvoyer un objet UserDetails à partir d'un nom d'utilisateur. Et ce, sans base de données, sans appels réseau et sans surprises : simplement depuis la mémoire du processus en cours.

Pour que cela ne ressemble pas à un « rituel », dessinons un schéma du niveau « ce qui appelle quoi » (sans analyse approfondie des classes internes) :

flowchart TD
    R["HTTP request"] --> F["Spring Security filters"]
    F --> AM["AuthenticationManager"]
    AM --> AP["AuthenticationProvider
(habituellement DaoAuthenticationProvider)"] AP --> UDS["UserDetailsService
(InMemoryUserDetailsManager)"] AP --> PE["PasswordEncoder"] PE --> AP UDS --> AP AP --> SC["SecurityContext (authenticated user)"]

Dans ce diagramme, une idée est importante pour nous : InMemoryUserDetailsManager n'est ni un « paramétrage des rôles » ni des « règles d'accès ». C'est un « annuaire téléphonique des utilisateurs » (on a parfois envie de dire un « cahier de logins ») que Spring Security utilise quand il a besoin de charger les données d'un compte.

4. Authentification in-memory : qu'est-ce que c'est

Quand nous disons « utilisateurs in-memory », cela ne signifie pas « pour de faux ». Cela signifie que la source des utilisateurs est définie à l'intérieur de l'application et vit aussi longtemps que votre processus JVM. Vous redémarrez l'application — la liste des utilisateurs est recréée à l'identique, exactement selon le code (ou la configuration) que vous avez écrit. Plus de « où est ma sauvegarde de base », plus de « pourquoi les migrations ne se sont pas appliquées ».

D'un point de vue pratique, le modèle in-memory offre des propriétés très utiles :

Premièrement, il se lit entièrement à l'œil. Ouvrez une classe de configuration — vous voyez quels utilisateurs existent et quels sont leurs droits. Pour un débutant, c'est inestimable : moins de magie, plus de contrôle.

Deuxièmement, il est parfait pour démontrer une matrice d'accès. Vous pouvez créer trois comptes (user/editor/admin) et vérifier manuellement ce qui est ouvert ou fermé. Et si quelque chose ne fonctionne pas, vous savez que le problème est presque certainement dans les règles d'accès ou dans la dénomination des rôles/autorités, pas dans la base de données.

Troisièmement, il discipline. Quand vous avez des utilisateurs scénarisés distincts, vous arrêtez de vivre en mode « donnons tout à tout le monde pour que ça ne gêne pas » — et vous commencez à voir les véritables limites d'accès.

Oui, le modèle in-memory a des limites évidentes, mais elles sont justement utiles pédagogiquement : il montre que les utilisateurs font partie de l'architecture de l'application, et non un simple « login quelque part ».

5. Premier UserDetailsService pour in-memory

Nous allons maintenant faire l'étape technique la plus importante du jour : donner à Spring Security sa propre source d'utilisateurs. Pour cela, il nous suffit de déclarer un @Bean de type UserDetailsService (ou précisément InMemoryUserDetailsManager). Dès qu'un tel bean apparaît dans le contexte, Spring Boot cesse de créer l'utilisateur par défaut avec un mot de passe généré — et commence à utiliser le nôtre.

Point clé : nous fournissons le mot de passe sous forme encodée. Nous n'écrivons pas {bcrypt}... manuellement. Nous utilisons le PasswordEncoder déjà configuré dans l'application, et nous appelons encode(...). Ainsi, le format sera correct, et toute « l'histoire des préfixes {id} » cessera d'être une théorie abstraite.

Ci-dessous, un démarrage minimal : un utilisateur reader. Ce n'est pas encore la matrice scénarisée du projet. Ici, nous avons besoin d'un seul fait : Spring Security utilise déjà notre UserDetailsService plutôt que l'utilisateur par défaut généré. Quand nous lierons les utilisateurs aux rôles et aux zones d'accès, ce démarrage minimal deviendra un ensemble complet de comptes scénarisés.

import org.springframework.context.annotation.Bean;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
UserDetailsService users(PasswordEncoder passwordEncoder) {
    // Création d'un utilisateur dans le format que Spring Security comprend "out of the box"
    UserDetails user = User.builder()
        .username("reader")
        // Le mot de passe est toujours stocké sous forme encodée, même si l'utilisateur ne vit qu'en mémoire
        .password(passwordEncoder.encode("reader-pass"))
        // Le rôle est nécessaire pour que l'utilisateur puisse correspondre aux règles d'accès dans SecurityFilterChain
        .roles("USER")
        .build();

    // InMemoryUserDetailsManager est notre source d'utilisateurs (UserDetailsService)
    return new InMemoryUserDetailsManager(user);
}

Il convient de noter deux choses ici.

Premièrement : nous utilisons User.builder() de Spring Security (org.springframework.security.core.userdetails.User). Ce n'est pas votre utilisateur métier du projet, ni UserAccount, ni UserProfile. C'est une représentation de sécurité de l'utilisateur que Spring Security comprend « nativement ». C'est un peu dommage que la classe s'appelle simplement User, mais c'est la vie : la sécurité aime les noms courts, pour que ce soit plus amusant lors du refactoring.

Deuxièmement : nous avons ajouté .roles("USER"). Nous n'abordons pas encore les rôles que nous aurons dans le projet – ce sera l'étape suivante. Ici, le rôle est simplement nécessaire pour que l'utilisateur puisse correspondre à vos règles d'accès existantes.

6. Remplissage de InMemoryUserDetailsManager

Quand il y a plus d'un utilisateur, on souhaite écrire le code de manière à ce qu'il reste lisible. InMemoryUserDetailsManager offre deux styles pratiques de remplissage. Le premier est de passer les utilisateurs dans le constructeur. Le second est de créer le gestionnaire vide et d'ajouter les utilisateurs via la méthode createUser(...). La seconde option est souvent plus agréable à lire quand vous construisez progressivement un ensemble de comptes scénarisés.

Le fragment suivant ne concerne pas non plus la matrice finale, mais la forme de stockage : InMemoryUserDetailsManager peut contenir un ensemble prédéfini de comptes et les augmenter au fur et à mesure que vous précisez le modèle d'accès.

Voici un exemple où nous créons explicitement le gestionnaire et y ajoutons un utilisateur. Remarquez : ici nous retournons InMemoryUserDetailsManager plutôt que l'interface UserDetailsService. Cela souligne que nous ne savons pas seulement « charger un utilisateur », mais aussi « gérer l'ensemble » (créer des utilisateurs en mémoire).

import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
InMemoryUserDetailsManager users(PasswordEncoder passwordEncoder) {
    // Le gestionnaire peut être créé vide et rempli d'utilisateurs comme une « liste de personnages »
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

    manager.createUser(User.builder()
        .username("editor")
        // Le hachage du mot de passe sera différent à chaque démarrage (à cause du sel), mais le mot de passe « brut » reste le même
        .password(passwordEncoder.encode("editor-pass"))
        // Les rôles doivent correspondre à ceux attendus par les règles d'autorisation
        .roles("USER", "EDITOR")
        .build());

    return manager;
}

Ce style est pratique car il ressemble à une « construction de scénarios ». Vous dites en quelque sorte : « dans notre projet pédagogique, il y a tel compte, et voici ses propriétés ». Plus tard, lorsque nous construirons une matrice d'accès complète, ce code se lira comme une « liste de personnages » qui parcourent votre API et vérifient où les portes sont ouvertes.

Un autre détail utile qui épargne des nerfs au débutant : passwordEncoder.encode("editor-pass") crée un nouveau hachage à chaque fois (surtout si c'est du bcrypt). Et c'est normal. Ce n'est pas que « le mot de passe change », c'est que la « représentation encodée du mot de passe change », car l'algorithme ajoute un sel. L'utilisateur se connecte toujours avec le même mot de passe en clair editor-pass.

7. Démarrage reproductible avec des utilisateurs in-memory

Après avoir ajouté votre propre UserDetailsService, il se produit une chose très importante : l'application devient déterministe. Hier, vous pouviez lancer le projet et obtenir à chaque fois un nouveau mot de passe généré dans les logs. Aujourd'hui, vous avez des comptes stables : reader, editor (et d'autres apparaîtront plus tard). Cela semble banal, mais en pratique cela signifie que chaque étudiant du groupe peut reproduire les étapes et obtenir le même résultat.

Du point de vue de Spring Boot, c'est aussi un « signal logiquement correct » : vous avez dit au framework « merci pour le mode démo, je prends le relais ». Boot cesse de proposer un utilisateur par défaut parce que vous avez déjà déclaré que l'application a son propre modèle d'utilisateurs.

Ce n'est pas encore la ligne de base complète de la journée, mais c'en est déjà le fondement obligatoire : nous avons désormais un magasin d'utilisateurs stable sur lequel nous pouvons sereinement définir les rôles, les droits ponctuels et les règles d'accès.

Et c'est là que se produit une magie pédagogique intéressante. Quand vous avez des utilisateurs fixes, vous pouvez regarder SecurityFilterChain comme une carte d'accès, et non comme un mystérieux patchwork de DSL. Vous commencez littéralement à lire la configuration comme ceci : « ces chemins sont publics ; ceux-ci sont accessibles uniquement aux utilisateurs authentifiés ; ceux-là seulement aux éditeurs » — et cela peut être vérifié avec des identifiants concrets.

Nous n'abordons pas encore les méthodes concrètes de connexion (quel mécanisme de login exactement et à quoi ressemble l'interface utilisateur/le flux). Ce qui importe maintenant est de comprendre une chose : sans vos propres utilisateurs, toutes les règles d'accès restent « sur le papier ».

8. Limites et pertinence du modèle in-memory

Le modèle in-memory a une limite, et elle n'est pas cachée : ce n'est pas un modèle pour un véritable produit public où les utilisateurs s'inscrivent, changent de mot de passe, sont bloqués par l'administrateur et vivent entre les redémarrages du service. Dans le monde in-memory, redémarrer l'application signifie littéralement « nous avons tout oublié et nous revivons selon la liste dans le code ».

Mais cela ne rend pas InMemoryUserDetailsManager inutile. Dans la vie réelle, on le rencontre souvent comme outil technique. Par exemple, pour des panneaux d'administration internes à un stade précoce, pour des prototypes, pour le développement local, pour de très petits services, pour des environnements de démonstration. Et, bien sûr, pour l'apprentissage — car il permet d'isoler la mécanique de sécurité de l'infrastructure.

Il est utile de garder en tête une simple comparaison (non pas comme « c'est mauvais/bon », mais comme « cela correspond à des tâches différentes ») :

Source d'utilisateurs Ce que c'est Avantages Inconvénients
Utilisateur par défaut du starter mode démo de Boot voir rapidement le secure-by-default mot de passe nouveau à chaque démarrage, vous ne contrôlez pas le modèle
InMemoryUserDetailsManager utilisateurs définis dans l'application reproductibilité, transparence, simplicité ne survit pas aux redémarrages, inadapté pour un cycle de vie réel des utilisateurs
Base de données / fournisseur externe véritable modèle de comptes réalisme, contrôlabilité, cycle de vie grande complexité et nombreux détails annexes

C'est précisément pourquoi le cours progresse par étapes : d'abord l'utilisateur par défaut (on voit l'effet), puis in-memory (on apprend à contrôler), et ensuite seulement la base de données (réalisme).

9. Erreurs typiques avec les utilisateurs in-memory

Erreur n°1 : « Puisque l'utilisateur est en mémoire, on peut stocker le mot de passe n'importe comment ».
Le piège le plus fréquent est le suivant : le développeur écrit .password("reader-pass") et est sincèrement surpris que la connexion ne fonctionne pas ou que Spring Security se plaigne du format du mot de passe. Le modèle in-memory ne change que l'emplacement de stockage de l'utilisateur, mais ne supprime pas les bonnes pratiques de sécurité de base. Le mot de passe doit être encodé, et la bonne manière est passwordEncoder.encode(...).

Erreur n°2 : Confusion entre « source d'utilisateurs » et « règles d'accès ».
Parfois, les étudiants essaient de « réparer l'accès » en ajoutant des rôles à un utilisateur alors que le problème se trouve en réalité dans SecurityFilterChain, ou inversement — ils réécrivent la chaîne alors qu'ils ont oublié d'ajouter l'utilisateur nécessaire. Une façon pratique d'éviter cela est simple : gardez toujours deux fichiers côte à côte et comparez-les visuellement. Dans l'un, vous décrivez les utilisateurs, dans l'autre, les règles. Ils doivent correspondre dans le « langage » (par exemple, les rôles), mais répondent à des questions différentes.

Erreur n°3 : Laisser un seul « super-utilisateur » qui peut tout faire.
C'est un raccourci pratique… jusqu'au moment où vous commencez à vérifier la matrice d'accès. Si vous avez un seul compte avec les droits maximaux, vous ne voyez pas les limites. Vous ne pouvez pas distinguer « tout est correctement fermé » de « tout est ouvert par hasard ». C'est pourquoi les utilisateurs scénarisés sont obligatoires : au moins un utilisateur normal et au moins un privilégié.

Erreur n°4 : Utiliser User.withDefaultPasswordEncoder() comme style principal.
Cette méthode existe comme démonstration pédagogique, mais elle crée une très mauvaise habitude : « les mots de passe sont encodés quelque part dans la magie ». Dans notre cours, l'approche correcte est d'avoir un bean PasswordEncoder explicite et un encode(...) explicite lors de la création des utilisateurs. Ainsi, vous vous habituez à un modèle réaliste, et non à une magie de tutoriel.

Erreur n°5 : Confondre org.springframework.security.core.userdetails.User avec l'utilisateur métier de l'application.
Dans les premières étapes, il est très facile d'écrire sa propre classe User dans le projet, puis d'importer soudainement « le mauvais User ». Le code a l'air normal, mais le compilateur se comporte comme un professeur strict : « mais ici, c'est un tout autre type ». La meilleure solution est de nommer les entités métier de manière plus spécifique (par exemple, UserAccount, UserProfile), et de considérer l'utilisateur de sécurité comme un adaptateur technique qui vit dans la couche de sécurité.

1
Mission
Spring Security, niveau 7, leçon 0
Bloqué
Un utilisateur in-memory et le chargement via `UserDetailsService`
Un utilisateur in-memory et le chargement via `UserDetailsService`
1
Mission
Spring Security, niveau 7, leçon 0
Bloqué
Point de terminaison public pour la lecture d'un utilisateur depuis `InMemoryUserDetailsManager`
Point de terminaison public pour la lecture d'un utilisateur depuis `InMemoryUserDetailsManager`
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION