Découpler le modèle utilisateur de votre application du système de sécurité de Symfony

Une approche de la sécurité avec Symfony qui sépare les utilisateurs de l’application des modèles de sécurité propres au framework.
Introduction
Lorsqu’on construit une application Symfony selon des modèles d’architecture avancés, comme l’architecture hexagonale, l’objectif principal est souvent de découpler le domaine (la logique métier) de l’infrastructure (les choix techniques). Cette séparation permet de se concentrer clairement sur les aspects métier de l’application sans les mêler excessivement à des implémentations techniques particulières.
Dans une configuration Symfony classique :
les autorisations sont toujours liées à un objet utilisateur. Pour sécuriser tout ou partie de votre application, vous devez créer une classe utilisateur. Cette classe implémente
UserInterface. Il s’agit souvent d’une entité Doctrine [4].
Cette approche est simple, mais elle crée un couplage fort entre le domaine et la couche d’infrastructure — ici, Symfony lui-même.
Plusieurs membres expérimentés de la communauté Symfony ont étudié différentes stratégies de découplage, en particulier pour la sécurité. L’une d’elles consiste à créer une classe exclusivement dédiée à la sécurité [1, 2, 3].
Cependant, la dernière étude technique approfondie sur ce sujet a été publiée il y a six ans, et le système de sécurité de Symfony a beaucoup évolué depuis. Dans cet article, je reviens sur ce problème et propose une approche actualisée.
Comment les séparer ?
-
Créer une classe utilisateur distincte pour la sécurité (
SecurityUser.php) -
Créer un fournisseur d’utilisateurs pour la sécurité (
SecurityUserProvider.php) -
Configurer Symfony pour utiliser le nouvel utilisateur de sécurité et son fournisseur dans
config/packages/security.yaml
SecurityUser : dans cette classe, je définis une entité utilisateur réservée à la sécurité et découplée de l’entité User de mon domaine. La classe implémente UserInterface ainsi que, si nécessaire, d’autres interfaces requises par le système de sécurité de Symfony.
SecurityUserProvider : le fournisseur d’utilisateurs est chargé de récupérer le SecurityUser à partir d’un identifiant, comme une adresse e-mail. Il peut charger l’utilisateur depuis une base de données ou toute autre source, généralement au moyen du modèle de domaine de votre application (l’entité User réelle).
<?php
declare(strict_types=1);
namespace Infrastructure\Framework\Symfony\Security;
use Domain\Model\User\Repository\UserRepository;
use Symfony\Component\Security\Core\{
Exception\UserNotFoundException,
User\UserInterface,
User\UserProviderInterface
};
final readonly class SecurityUserProvider implements UserProviderInterface
{
public function __construct(
private UserRepository $userRepository
) {
}
#[\Override]
public function refreshUser(UserInterface $user): UserInterface
{
return $this->loadUserByIdentifier($user->getUserIdentifier());
}
#[\Override]
public function loadUserByIdentifier(string $identifier): UserInterface
{
$user = $this->userRepository->getByEmail($email);
if ($user === null) {
throw new UserNotFoundException();
}
return SecurityUser::create($user);
}
#[\Override]
public function supportsClass(string $class): bool
{
return $class === SecurityUser::class;
}
}Il ne reste plus qu’à déclarer le fournisseur d’utilisateurs auprès de Symfony dans config/packages/security.yaml :
security:
# ...
providers:
app_user_provider:
id: Infrastructure\Framework\Symfony\Security\SecurityUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
logout:
path: app_logout
target: app_login
access_control:
- { path: ^/login$, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER }Conclusion
Symfony propose plusieurs authentificateurs intégrés, comme form_login, qui simplifient l’implémentation de l’authentification des utilisateurs. Si votre application exige toutefois une logique d’authentification plus complexe, vous pouvez créer un authentificateur personnalisé et vous appuyer sur le SecurityUserProvider que j’ai implémenté pour charger les utilisateurs pendant le processus d’authentification.
public function authenticate(Request $request): Passport
{
$token = $request->request->get('_csrf_token');
$email = $request->request->get('email');
$password = $request->request->get('password');
$request
->getSession()
->set(SecurityRequestAttributes::LAST_USERNAME, $email);
$passport = new Passport(
new UserBadge($email, $this->securityUserProvider->loadUserByIdentifier(...)),
new PasswordCredentials($password),
[
new CsrfTokenBadge('authenticate', $token),
new RememberMeBadge(),
]
);
return $passport;
}Il est important de noter que, lorsque vous récupérez l’utilisateur actuellement connecté — par exemple avec la méthode getUser() ou app.user dans Twig — Symfony renvoie l’objet SecurityUser créé précédemment, et non le User de votre domaine. Cette distinction est essentielle : SecurityUser sert uniquement à l’authentification et à l’autorisation, ce qui maintient ma logique métier découplée de l’infrastructure Symfony.
Si vous avez besoin d’informations supplémentaires provenant de l’entité User réelle — comme les données du profil ou d’autres informations propres au domaine — il est recommandé d’utiliser un modèle de vue. Celui-ci sert de passerelle : il récupère les données nécessaires depuis l’entité User et les expose sous une forme exploitable par votre application, sans rompre la séparation entre le domaine et la couche d’infrastructure.
Bon développement !