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

Séparer l’utilisateur du domaine de l’objet attendu par Symfony Security avec un fournisseur dédié.
Une architecture hexagonale garde les règles métier indépendantes des choix techniques. Pourtant, l’implémentation habituelle de Symfony Security pousse souvent l’entité utilisateur du domaine à implémenter une interface du framework.
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 couple le domaine à Symfony.
Une autre approche consiste à créer une classe réservée à la sécurité [1, 2, 3].
Les articles cités en référence précèdent plusieurs évolutions de Symfony Security. Voici la même séparation avec les API actuelles.
Séparer les deux modèles
-
Créer une classe utilisateur distincte pour la sécurité,
SecurityUser.php. -
Créer son fournisseur,
SecurityUserProvider.php. -
Configurer les deux services dans
config/packages/security.yaml.
SecurityUser. Cette classe représente l’utilisateur attendu par Symfony sans modifier l’entité User du domaine. Elle implémente UserInterface et les autres interfaces requises par le système de sécurité.
SecurityUserProvider. Le fournisseur d’utilisateurs récupère SecurityUser à partir d’un identifiant, ici une adresse e-mail. Il charge d’abord l’entité User depuis le dépôt du domaine, puis l’adapte au type attendu par Symfony.
<?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 }Accéder aux données du domaine après la connexion
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;
}Lorsque vous récupérez l’utilisateur connecté avec getUser() ou app.user dans Twig, Symfony renvoie SecurityUser, pas le User du domaine. SecurityUser sert uniquement à l’authentification et à l’autorisation.
Si une vue a besoin du profil ou d’autres données métier, chargez un modèle de lecture dédié depuis l’identifiant de SecurityUser. Le contrôleur obtient les données nécessaires sans exposer l’entité du domaine à Symfony Security.