1. Du filtre au provider
La vraie vérification de l'utilisateur ne vit pas dans le manager, mais dans le provider. L'AuthenticationManager a déjà montré l'essentiel : il accepte l'Authentication attempt et lance la vérification. Mais le manager lui-même ne recherche pas l'utilisateur, ne compare pas le mot de passe et ne décide pas quels credentials considérer valides. Si cette partie n'est pas isolée séparément, l'authentification se dispersera de nouveau dans les filtres, les contrôleurs et des services aléatoires.
C'est pourquoi le nœud suivant de la chaîne est l'AuthenticationProvider. C'est là que vit la vraie vérification : comprendre si tu sais travailler avec ce type d'Authentication, et soit retourner un résultat confirmé, soit refuser. On garde le focus sur cet endroit précis, sans diluer le provider dans le schéma complet de la requête.
2. AuthenticationProvider : contrat et responsabilité
L'AuthenticationProvider est le point où Spring Security s'attend à voir une vérification concrète. En termes de code, c'est une interface qui répond à deux questions : « sais-tu travailler avec ce type d'Authentication ? » et « si oui — vérifie et retourne le résultat ». L'important est que le provider vit dans le monde du Java pur, pas dans le monde HTTP. Ça permet de réutiliser la même vérification dans différents scénarios web sans lier la sécurité aux contrôleurs.
L'interface a deux méthodes clés. La première — authenticate(...). Elle reçoit une Authentication (généralement une tentative de connexion) et doit retourner une Authentication réussie (déjà "authenticated") ou lancer une AuthenticationException si la vérification a échoué. La deuxième méthode — supports(...) : elle est nécessaire pour que le ProviderManager puisse choisir « le bon spécialiste », sans aller frapper à toutes les portes.
Voici le mini-squelette d'un provider (ce n'est pas une « solution prête », c'est la forme de la pensée) :
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
public class MyAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) {
// IMPORTANT : ici le provider effectue la vraie vérification "oui/non" (sans HTTP)
// C'est un stub pour montrer la forme du contrat.
return null;
}
@Override
public boolean supports(Class<?> authenticationType) {
// IMPORTANT : ici le provider déclare quels types d'Authentication il sait traiter
// Le ProviderManager utilise ça pour choisir "le bon spécialiste".
return false;
}
}
Faisons maintenant un « provider-parodie » pédagogique pour voir l'essentiel de la mécanique. Il n'acceptera que le login demo et le mot de passe demo. Dans une vraie application, on ne peut pas faire ça (et c'est très bien : sinon beaucoup l'auraient fait).
import java.util.List;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
public Authentication authenticate(Authentication auth) {
// principal se lit généralement via getName() / getPrincipal() — c'est le "qui"
String username = auth.getName();
// credentials se trouvent généralement dans getCredentials() — c'est le "secret" (par exemple, le mot de passe)
Object credentials = auth.getCredentials();
// Vérification pédagogique (et très mauvaise pour le vrai monde)
if (!"demo".equals(username) || !"demo".equals(credentials)) {
// Si la vérification échoue — on lance un descendant d'AuthenticationException
throw new BadCredentialsException("Bad credentials");
}
// Dans un Authentication réussi, on ne stocke généralement PAS le mot de passe (d'où le null)
return UsernamePasswordAuthenticationToken.authenticated("demo", null, List.of());
}
Ici, l'important n'est pas de mémoriser "demo/demo", mais de voir la forme. Le provider prend l'Authentication en entrée, extrait le principal (via getName() ou getPrincipal()) et les credentials (via getCredentials()), les vérifie et retourne un nouvel Authentication avec le marqueur « vérifié ». Remarque le null en second argument : dans un résultat réussi, on ne stocke généralement pas le mot de passe pour ne pas l'entraîner accidentellement dans le code. Le mot de passe n'est pas un souvenir, il n'a pas besoin de voyager avec toi dans l'application.
L'idée clé est simple : le provider est l'endroit où vit la logique "oui / non" de l'authentification. Si tu fais ça dans le contrôleur, tu dilues la logique dans la couche web. Si tu le fais dans le filtre, tu mélangles l'extraction des données HTTP et la vérification elle-même. Spring Security est conçu différemment : le filtre assemble la tentative, le provider vérifie.
3. supports(...) et plusieurs providers
La méthode supports(...) semble être de la bureaucratie ennuyeuse, jusqu'à ce qu'on voie l'image dans son ensemble. Elle est nécessaire parce qu'une application peut avoir plusieurs providers, et chacun se spécialise dans son type d'Authentication. C'est comme un assortiment de chargeurs différents : un pour USB-C, un pour le vieux micro-USB, un troisième pour l'ordinateur portable. Si on « enfonce n'importe quel chargeur n'importe où », on ne chargera rien, ou on cassera la prise — ce qui est pire, mais tellement humain.
À l'intérieur du ProviderManager, la logique de sélection ressemble à peu près à ceci (simplifié, mais honnête dans le sens) :
for (AuthenticationProvider provider : providers) {
// ProviderManager filtre d'abord les providers par type d'Authentication
if (provider.supports(authentication.getClass())) {
// A trouvé le "bon" — il lui délègue la vraie vérification
return provider.authenticate(authentication);
}
}
// Si personne ne supporte ce type d'Authentication — pas moyen d'authentifier
throw new RuntimeException("No suitable provider");
C'est-à-dire que le ProviderManager n'est ni devin ni médium. Il parcourt littéralement la liste des providers et demande : « Tu sais faire ça ? ». Si oui — vérifie. Si non — je passe. Ça donne une liberté d'ingénierie très importante : la couche web peut créer différents types d'Authentication selon le scénario, et le manager trouvera le provider qui sait travailler avec ce type.
Pour ce cours, l'essentiel à comprendre est : supports(...) c'est « la façon dont le provider déclare sa spécialisation ». Généralement dans supports(), tu verras une vérification via isAssignableFrom(...), pour que le provider supporte non seulement exactement une classe, mais aussi ses sous-classes :
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
public boolean supports(Class<?> type) {
// isAssignableFrom(...) permet de supporter aussi les sous-classes du type indiqué
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(type);
}
Et encore une pensée soignée. Le provider peut être « expert en vérification d'identité », mais il ne doit pas décider quelle URL est accessible à qui. Même s'il a retourné une Authentication avec certaines authorities, ça ne signifie pas encore « on peut tout faire ». Ça signifie juste « on est certain de qui c'est ». L'autorisation vit à un autre niveau, et si tu commences à mettre des "règles d'accès" dans le provider, le système deviendra à la fois imprévisible et difficile à tester.
4. DaoAuthenticationProvider pour username/password
Le DaoAuthenticationProvider est le provider le plus important du cours, parce qu'il implémente un modèle basique et très répandu : « l'utilisateur entre un login et un mot de passe, le serveur vérifie ». Si l'AuthenticationProvider est un « contrat d'expert » abstrait, le DaoAuthenticationProvider est déjà un expert concret, qui sait travailler selon le schéma standard. Presque toujours, c'est lui qu'on utilise en premier, et on décide ensuite si quelque chose doit être étendu.
Pourquoi dao dans le nom ? Parce que le provider ne stocke pas les utilisateurs en lui-même. Il charge les données de l'utilisateur via un composant séparé (dans Spring Security c'est le UserDetailsService). Son modèle est donc : « donne-moi un login → je vais dans le user store → j'obtiens les informations sur l'utilisateur → je vérifie le mot de passe → je retourne le résultat ». Le user store peut être n'importe où : en mémoire, dans une base de données, dans un LDAP (qu'on n'utilise pas dans ce cours), mais l'idée reste la même : le provider ne sait pas où tu stockes les utilisateurs, il connaît seulement le contrat de chargement.
Pour cette étape, ce qui nous importe n'est pas toute la requête, mais précisément le morceau interne du travail du provider : il reçoit une Authentication attempt déjà préparée, charge l'utilisateur, vérifie le mot de passe et retourne le résultat. En version compacte, ça ressemble à ceci :
flowchart TD
A["Tentative d'authentification"] --> D["DaoAuthenticationProvider"]
D --> E["UserDetailsService : charger l'utilisateur"]
E --> U["UserDetails"]
U --> D
D --> F["PasswordEncoder : comparer le mot de passe"]
F --> G["Résultat d'authentification"]
Oui, il y a un PasswordEncoder dans le schéma. On ne l'a pas encore étudié en détail, mais ce qui compte ici c'est seulement sa place : le provider fait la vérification, et l'encoder fournit la façon de comparer correctement le mot de passe saisi avec ce qui est stocké chez l'utilisateur.
Encore un détail utile : si tu as connecté le spring-boot-starter-security sans rien configurer, il y a de fortes chances que quelque part dans les entrailles de ton application tourne déjà précisément le DaoAuthenticationProvider (en liaison avec le UserDetailsService de démarrage que Spring Boot monte pour l'utilisateur par défaut). Autrement dit, on étudie non pas une « bête rare », mais le fondement typique.
5. Les vérifications dans DaoAuthenticationProvider
Quand les débutants entendent « vérification du login et du mot de passe », ils l'imaginent comme une seule ligne if(password.equals(...)). En réalité, même dans le modèle de base, il y a plus d'étapes, et Spring Security les effectue pour toi, mais comprendre leur sens est utile. On n'ira pas dans la cryptographie (c'est un niveau séparé), on va juste décomposer les vérifications par sens — pour qu'une image claire « pourquoi ça peut ne pas fonctionner » apparaisse dans la tête.
Première catégorie de vérifications — « l'utilisateur existe et peut être chargé ». Le DaoAuthenticationProvider demande au UserDetailsService de trouver l'utilisateur par son nom. Si l'utilisateur n'existe pas, l'authentification ne peut pas réussir. Une erreur typique apparaît souvent ici : le développeur pense que le problème vient du mot de passe, alors qu'en fait le UserDetailsService n'a simplement pas trouvé l'utilisateur — à cause d'une faute de frappe, de la casse, d'un mauvais identifiant ou parce que le user store est vide.
Deuxième catégorie de vérifications — « le mot de passe convient ». Le provider prend ce qui est arrivé comme credentials (généralement le mot de passe brut de la tentative) et le compare avec ce qui est stocké chez l'utilisateur (généralement pas le mot de passe brut, mais une valeur encodée/hashée). Pour cette partie, on utilise le PasswordEncoder. Ce qui compte aujourd'hui c'est seulement sa place dans l'architecture : le provider fait la vérification, l'encoder est l'« outil de comparaison ».
Troisième catégorie de vérifications — « le compte est en ordre ». Même si le login/mot de passe sont corrects, le compte peut être désactivé, bloqué, etc. Ces flags viennent du UserDetails ; ce qui nous importe ici est le fait lui-même : le provider ne regarde pas seulement le mot de passe, mais aussi l'état du compte.
Et enfin, si tout va bien, le provider crée le résultat — une Authentication qui contient déjà le principal et les authorities, et ce résultat ira ensuite dans le SecurityContext. Dans un résultat réussi, le provider ne conserve généralement pas le mot de passe, parce que le mot de passe en tant que secret ne doit pas voyager dans l'application.
Pour voir la différence « avant/après » de façon très compacte, on peut imaginer ce bout de code (ce n'est pas quelque chose que tu écriras tous les jours, c'est juste une illustration d'état) :
import org.springframework.security.core.Authentication;
// attempt : ce qui est arrivé en entrée (généralement username+password)
Authentication attempt = /* username+password */;
// result : ce que le provider retourne en cas de succès (principal+authorities)
Authentication result = /* authenticated principal+authorities */;
System.out.println(attempt.isAuthenticated()); // false
System.out.println(result.isAuthenticated()); // true
Si on garde en tête précisément cette différence, beaucoup de situations « étranges » dans les logs cessent d'être mystérieuses. Tu as une tentative, tu as un résultat, et c'est précisément le provider qui est responsable de la transformation de l'un en l'autre.
6. Mini-liaison : ProviderManager et DaoAuthenticationProvider
Quand le DaoAuthenticationProvider cesse d'être une abstraction, il est plus facile de le percevoir comme un bean Spring ordinaire : un bean est responsable de la façon de vérifier, un autre assemble l'AuthenticationManager par-dessus ce provider. Dans Secure Content Platform API, la partie security est commode à garder sous com.example.securecontent.security....
On commence par le provider. On veut montrer explicitement : le DaoAuthenticationProvider est un AuthenticationProvider qui a besoin d'une source d'utilisateurs via le UserDetailsService. Aujourd'hui, on ne détaille pas la structure du UserDetailsService, donc on l'accepte simplement comme une dépendance que Spring nous fournira depuis le contexte (y compris, à l'étape du starter-security, il y a déjà une implémentation de base).
package com.example.securecontent.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsService;
@Configuration
public class SecurityProvidersConfig {
@Bean
AuthenticationProvider authenticationProvider(UserDetailsService users) {
// On crée le provider standard pour username/password
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// On connecte la source d'utilisateurs (d'où charger UserDetails par username)
provider.setUserDetailsService(users);
// Note : PasswordEncoder est généralement configuré séparément (dans une autre configuration)
return provider;
}
}
Ici surgit immédiatement la question suivante. Le provider n'a pas besoin d'un « utilisateur abstrait du domaine », mais d'un objet qui contient déjà le username, le mot de passe, les authorities et l'état du compte dans une forme que Spring Security comprend.
Remarque la « pureté méthodologique » importante de cette construction. Il n'y a pas de HTTP, pas de contrôleurs, pas de lecture de headers, pas de JSON. Juste un provider et la dépendance « donne des utilisateurs ». C'est exactement ainsi que ça doit être : le provider n'est pas la couche web.
Maintenant le manager. Dans la leçon 2, on a discuté que le ProviderManager est l'implémentation typique de l'AuthenticationManager, qui coordonne un ou plusieurs providers. En code, ça ressemble à quelque chose de très ennuyeux — et dans la sécurité, l'ennuyeux signifie souvent « correct ».
package com.example.securecontent.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.AuthenticationProvider;
@Configuration
public class SecurityAuthenticationConfig {
@Bean
AuthenticationManager authenticationManager(AuthenticationProvider provider) {
// AuthenticationManager est de la coordination, pas la "vraie vérification"
// Il la délègue à l'intérieur du/des provider(s)
return new ProviderManager(provider);
}
}
Remarque : l'AuthenticationManager est assemblé par-dessus l'AuthenticationProvider, pas l'inverse. Ça souligne encore une fois l'idée du jour : le manager ne vit pas séparément de la vraie vérification, il l'organise.
Si tu veux « palper de tes mains » que les providers existent vraiment dans le contexte, on peut ajouter un petit CommandLineRunner qui imprime la liste des providers au démarrage. C'est une bonne astuce pédagogique : elle ne casse rien, mais rend l'abstraction visible.
package com.example.securecontent.security.debug;
import java.util.List;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.stereotype.Component;
@Component
class AuthProvidersDebugRunner implements CommandLineRunner {
// Spring rassemblera ici tous les beans AuthenticationProvider du contexte
private final List<AuthenticationProvider> providers;
AuthProvidersDebugRunner(List<AuthenticationProvider> providers) {
this.providers = providers;
}
@Override
public void run(String... args) {
// Diagnostic pédagogique : montrer quels providers ont vraiment démarré
providers.forEach(p -> System.out.println(p.getClass().getSimpleName()));
// exemple de sortie : DaoAuthenticationProvider
}
}
Oui, dans un vrai projet on ne fait pas ça (les logs sont une culture séparée), mais pour l'apprentissage, ça aide : tu as le sentiment que le provider n'est pas un terme théorique, mais un objet réel qui vit dans l'application et participe à l'authentification.
Et encore une liaison importante avec le projet. Dans Secure Content Platform API, on aura plusieurs zones : publique, personnelle, éditeur, admin. Et pour accéder à au moins une zone non publique, on a d'abord besoin d'un utilisateur authentifié. C'est cette partie — « comment obtenir un utilisateur authentifié » — qu'on construit maintenant : le filtre lance le processus, le manager choisit la stratégie, le provider fait la vraie vérification. Sans ça, toute discussion sur l'accès à /api/me se transforme en débat « peut-on laisser entrer quelqu'un dans l'appartement si on ne sait pas encore qui c'est ».
7. Erreurs fréquentes avec les providers
Comprendre les providers, c'est comme apprendre à distinguer le « bouton marche/arrêt » de la « rallonge ». Après ça, beaucoup de problèmes mystérieux cessent d'être mystérieux, mais de nouveaux apparaissent — d'ingénierie cette fois. Et c'est un bon signe : tu as arrêté de croire à la magie et tu commences à voir le système.
Erreur n°1 : essayer de vérifier le login/mot de passe dans le contrôleur ou le service.
Ça commence généralement par un inoffensif if(password.equals(...)) dans un AuthController, et ça se termine avec un second contrôleur, un troisième endpoint, et la logique de vérification, de gestion des erreurs, des blocages et tout le reste copiée-collée partout. Spring Security fait exprès du provider le point central de vérification pour que tu ne fasses pas de « copier-coller de sécurité ».
Erreur n°2 : confondre provider et filter selon la responsabilité.
Le filtre doit extraire les données de la requête HTTP et créer la tentative d'Authentication. Le provider doit vérifier que la tentative est valide. Quand le provider commence à analyser les headers, et que le filtre commence à aller dans le UserDetailsService, on obtient un millefeuille qui s'effondre au premier changement de scénario.
Erreur n°3 : implémenter incorrectement supports(...).
Si supports() retourne toujours true, ton provider essaiera de traiter absolument tout, y compris les types d'Authentication pour lesquels il n'est pas fait. Si supports() retourne toujours false, le provider sera « invisible », et le ProviderManager ne le sélectionnera tout simplement pas. Dans les deux cas, les symptômes ressemblent à « pourquoi le manager ne fonctionne pas », alors que le problème vient du fait que le provider est soit trop gourmand, soit trop modeste.
Erreur n°4 : mélanger authentification et autorisation à l'intérieur du provider.
Le provider répond à « qui es-tu », pas à « que peut-on faire ». Il peut mettre des authorities dans le résultat pour que la chaîne puisse prendre des décisions ensuite, mais essayer à l'intérieur du provider de décider « laisser ou non passer vers /api/admin » est une erreur architecturale. Tu perdras la lisibilité, la réutilisabilité et un diagnostic normal de 401/403.
Erreur n°5 : écrire un provider « pédagogique » et l'oublier accidentellement dans le projet comme un provider réel.
Des trucs comme demo/demo sont utiles pour comprendre la mécanique, mais dangereux comme habitude. Dans une vraie application, le provider doit s'appuyer sur un vrai user store et une vraie vérification de mot de passe. Si tu as besoin de « faire une connexion rapidement », il est presque toujours plus juste d'utiliser le DaoAuthenticationProvider et de connecter correctement un UserDetailsService, plutôt que d'écrire une vérification maison.
GO TO FULL VERSION