.. index:: single: Sécurité; Fournisseur d'utilisateur single: Sécurité; Fournisseur d'entité
La couche de sécurité est l'un des outils les plus intelligents de Symfony. Il gère deux choses : les procédés d'authentification et d'autorisation. Bien qu'il puisse être difficile de comprendre comment cela fonctionne en interne, le système de sécurité est très flexible et vous permet d'intégrer votre application avec n'importe quel « backend » d'authentification, comme Active Directory, un serveur OAuth ou une base de données.
Cet article traite de l'authentification des utilisateurs à travers
une table de base de données gérée par une classe entité Doctrine. Le contenu
de cet article du Cookbook est divisé en trois parties. La première partie
parle de la conception d'une classe entité Doctrine User
ainsi que du
fait de la rendre utilisable par la couche de sécurité de Symfony. La deuxième
partie décrit comment authentifier facilement un utilisateur avec l'objet Doctrine
:class:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider` fourni
avec le « framework » et quelques éléments de configurations. Finalement, le
tutoriel va démontrer comment créer un objet personnalisé
:class:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider` pour
récupérer des utilisateurs depuis la base de données sous certaines
conditions personnalisées.
Ce tutoriel suppose qu'il existe un bundle Acme\UserBundle
démarré et chargé
dans le kernel de l'application.
Pour cet article du cookbook, le bundle AcmeUserBundle
contient une classe
entité User
avec les champs suivants : id
, username
, salt
,
password
, email
et isActive
. Le champ isActive
précise si oui
ou non le compte de l'utilisateur est activé.
Pour faire court, les méthodes « getter » et « setter » de chacun des champs ont été supprimées pour se concentrer sur les méthodes les plus importantes qui proviennent de l'interface :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Acme\UserBundle\Entity\User
*
* @ORM\Table(name="acme_users")
* @ORM\Entity(repositoryClass="Acme\UserBundle\Entity\UserRepository")
*/
class User implements UserInterface, \Serializable
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", length=25, unique=true)
*/
private $username;
/**
* @ORM\Column(type="string", length=32)
*/
private $salt;
/**
* @ORM\Column(type="string", length=40)
*/
private $password;
/**
* @ORM\Column(type="string", length=60, unique=true)
*/
private $email;
/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
public function __construct()
{
$this->isActive = true;
$this->salt = md5(uniqid(null, true));
}
/**
* @inheritDoc
*/
public function getUsername()
{
return $this->username;
}
/**
* @inheritDoc
*/
public function getSalt()
{
return $this->salt;
}
/**
* @inheritDoc
*/
public function getPassword()
{
return $this->password;
}
/**
* @inheritDoc
*/
public function getRoles()
{
return array('ROLE_USER');
}
/**
* @inheritDoc
*/
public function eraseCredentials()
{
}
/**
* @see \Serializable::serialize()
*/
public function serialize()
{
return serialize(array(
$this->id,
));
}
/**
* @see \Serializable::unserialize()
*/
public function unserialize($serialized)
{
list (
$this->id,
) = unserialize($serialized);
}
}
Afin d'utiliser une instance de la classe AcmeUserBundle:User
dans la couche
de sécurité de Symfony, la classe entité doit implémenter l'interface
:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. Cette interface
force la classe à implémenter les cinq méthodes suivantes :
* getRoles()
,
* getPassword()
,
* getSalt()
,
* getUsername()
,
* eraseCredentials()
Pour plus de détails sur chacune d'entre elles, voir :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
Note
L'interface :phpclass:`Serializable` ainsi que ses méthodes serialize
et unserialize
ont été ajoutées pour permettre à la classe User
d'être sérialisable
dans la session. Cela peut ou non être nécessaire en fonction de votre configuration,
mais c'est certainement une bonne idée. Seule la propriété id
a besoin d'être
sérialisée, car la méthode :method:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider::refreshUser`
recharge l'utilisateur à chaque requête en utilisant la propriété id
.
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
use Symfony\Component\Security\Core\User\EquatableInterface;
// ...
public function isEqualTo(UserInterface $user)
{
return $this->username === $user->getUsername();
}
Voici, ci-dessous, un export de ma table User
depuis MySQL. Pour plus de détails sur
la création des entrées utilisateur et l'encodage de leur mot de passe, lisez le
chapitre :ref:`book-security-encoding-user-password`.
mysql> select * from user;
+----+----------+----------------------------------+------------------------------------------+--------------------+-----------+
| id | username | salt | password | email | is_active |
+----+----------+----------------------------------+------------------------------------------+--------------------+-----------+
| 1 | hhamon | 7308e59b97f6957fb42d66f894793079 | 09610f61637408828a35d7debee5b38a8350eebe | [email protected] | 1 |
| 2 | jsmith | ce617a6cca9126bf4036ca0c02e82dee | 8390105917f3a3d533815250ed7c64b4594d7ebf | [email protected] | 1 |
| 3 | maxime | cd01749bb995dc658fa56ed45458d807 | 9764731e5f7fb944de5fd8efad4949b995b72a3c | [email protected] | 0 |
| 4 | donald | 6683c2bfd90c0426088402930cadd0f8 | 5c3bcec385f59edcc04490d1db95fdb8673bf612 | [email protected] | 1 |
+----+----------+----------------------------------+------------------------------------------+--------------------+-----------+
4 rows in set (0.00 sec)
La base de données contient désormais quatre utilisateurs avec différents noms d'utilisateurs, emails et statuts. La prochaine partie va traiter de l'authentification de l'un de ces utilisateurs grâce au fournisseur d'entité utilisateur Doctrine et à quelques lignes de configuration.
Authentifier un utilisateur Doctrine à travers une base de données avec la
couche de sécurité de Symfony est vraiment très facile. Tout réside dans la
configuration du :doc:`SecurityBundle</reference/configuration/security>`
stockée dans le fichier app/config/security.yml
.
Vous trouverez ci-dessous un exemple de configuration où l'utilisateur va entrer son nom d'utilisateur et son mot de passe via une authentification basique HTTP. Cette information sera alors comparée et vérifiée avec nos entrées d'entité « User » de la base de données :
.. configuration-block:: .. code-block:: yaml # app/config/security.yml security: encoders: Acme\UserBundle\Entity\User: algorithm: sha1 encode_as_base64: false iterations: 1 role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ] providers: administrators: entity: { class: AcmeUserBundle:User, property: username } firewalls: admin_area: pattern: ^/admin http_basic: ~ access_control: - { path: ^/admin, roles: ROLE_ADMIN }
La section encoders
associe l'encodeur de mot de passe sha1
à la classe
entité. Cela signifie que Symfony va s'attendre à ce que le mot de passe stocké
dans la base de données soit encodé à l'aide de cet algorithme. Pour plus de détails
sur la création d'un nouvel objet « User » avec un mot de passe encrypté
correctement, lisez la section :ref:`book-security-encoding-user-password` du
chapitre sur la sécurité.
La section providers
définit un fournisseur d'utilisateur administrators
.
Un fournisseur d'utilisateur est une « source » indiquant où les utilisateurs
sont chargés lors de l'authentification. Dans ce cas, le mot-clé entity
signifie que Symfony va utiliser le fournisseur d'entité utilisateur Doctrine
pour charger des objets entité « User » depuis la base de données en utilisant
le champ unique username
. En d'autres termes, cela indique à Symfony
comment récupérer l'utilisateur depuis la base de données avant de vérifier
la validité du mot de passe.
Ce code et cette configuration fonctionnent mais ce n'est pas suffisant pour
sécuriser l'application pour des utilisateurs activés. En effet, maintenant,
vous pouvez toujours vous authentifier avec maxime
. La section suivante
explique comment interdire l'accès aux utilisateurs non-activés.
La manière la plus facile d'exclure des utilisateurs non-activés est
d'implémenter l'interface
:class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface`
qui se charge de vérifier le statut du compte de l'utilisateur.
L'interface :class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface`
étend l'interface :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`,
donc vous devez simplement utiliser la nouvelle interface dans la classe
entité AcmeUserBundle:User
afin de bénéficier de comportements
d'authentification simples et avancés.
L'interface :class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface` ajoute quatre méthodes supplémentaires pour valider le statut du compte :
isAccountNonExpired()
vérifie si le compte de l'utilisateur a expiré,isAccountNonLocked()
vérifie si l'utilisateur est verrouillé,isCredentialsNonExpired()
vérifie si les informations de connexion de l'utilisateur (mot de passe) ont expiré,isEnabled()
vérifie si l'utilisateur est activé.
Pour cet exemple, les trois premières méthodes vont retourner true
alors
que la méthode isEnabled()
va retourner la valeur booléenne du champ
isActive
.
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
// ...
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
class User implements AdvancedUserInterface, \Serializable
{
// ...
public function isAccountNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
return true;
}
public function isCredentialsNonExpired()
{
return true;
}
public function isEnabled()
{
return $this->isActive;
}
}
Si vous essayez de vous authentifier avec maxime
, l'accès est maintenant
interdit puisque cet utilisateur n'a pas un compte activé. La prochaine section
va se concentrer sur l'implémentation d'un fournisseur d'entité personnalisé
pour authentifier un utilisateur avec son nom d'utilisateur ou avec son adresse
email.
La prochaine étape est de permettre à un utilisateur de s'authentifier avec son nom d'utilisateur ou avec son adresse email comme ils sont tous les deux uniques dans la base de données. Malheureusement, le fournisseur d'entité natif est seulement capable de gérer une propriété unique pour récupérer l'utilisateur depuis la base de données.
Pour réaliser ceci, créez un fournisseur d'entité personnalisé qui cherche
un utilisateur dont le champ « nom d'utilisateur » ou « email » correspond
au nom d'utilisateur soumis lors de la phase de connexion/login. La bonne
nouvelle est qu'un objet Repository Doctrine peut agir comme un fournisseur
d'entité utilisateur s'il implémente
:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`.
Cette interface est fournie avec trois méthodes à implémenter :
loadUserByUsername($username)
, refreshUser(UserInterface $user)
,
et supportsClass($class)
. Pour plus de détails, lisez
:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`.
Le code ci-dessous montre l'implémentation de
:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface` dans
la classe UserRepository
:
// src/Acme/UserBundle/Entity/UserRepository.php namespace Acme\UserBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\NoResultException; class UserRepository extends EntityRepository implements UserProviderInterface { public function loadUserByUsername($username) { $q = $this ->createQueryBuilder('u') ->where('u.username = :username OR u.email = :email') ->setParameter('username', $username) ->setParameter('email', $username) ->getQuery(); try { // La méthode Query::getSingleResult() lance une exception // s'il n'y a pas d'entrée correspondante aux critères $user = $q->getSingleResult(); } catch (NoResultException $e) { throw new UsernameNotFoundException(sprintf('Unable to find an active admin AcmeUserBundle:User object identified by "%s".', $username), 0, $e); } return $user; } public function refreshUser(UserInterface $user) { $class = get_class($user); if (!$this->supportsClass($class)) { throw new UnsupportedUserException( sprintf( 'Instances of "%s" are not supported.', $class ) ); } return $this->find($user->getId()); } public function supportsClass($class) { return $this->getEntityName() === $class || is_subclass_of($class, $this->getEntityName()); } }
Pour finir l'implémentation, la configuration de la couche de sécurité doit
être modifiée pour dire à Symfony d'utiliser le nouveau fournisseur d'entité
personnalisé à la place du fournisseur d'entité Doctrine générique. Ceci est
facile à réaliser en supprimant le champ property
dans la section
security.providers.administrators.entity
du fichier security.yml
.
.. configuration-block:: .. code-block:: yaml # app/config/security.yml security: # ... providers: administrators: entity: { class: AcmeUserBundle:User } # ...
En faisant cela, la couche de sécurité va utiliser une instance de
UserRepository
et appeler sa méthode loadUserByUsername()
pour récupérer
un utilisateur depuis la base de données, qu'il ait saisi son nom d'utilisateur
ou son adresse email.
La fin de ce tutoriel se concentre sur comment stocker et récupérer une liste
de rôles depuis la base de données. Comme précisé précédemment, lorsque votre
utilisateur est « chargé », sa méthode getRoles()
retourne le tableau contenant
ses rôles de sécurité qui doivent lui être assignés. Vous pouvez charger ces
données depuis n'importe où - une liste codée en dur et utilisée pour tous les
utilisateurs (par exemple : array('ROLE_USER')
), un tableau Doctrine en tant
que propriété nommée roles
, ou via une relation Doctrine, comme vous allez
le voir dans cette section.
Caution!
Avec une installation typique, vous devriez toujours retourner au moins 1 rôle
avec la méthode getRoles()
. Par convention, un rôle appelé ROLE_USER
est généralement retourné. Si vous ne réussissez pas à retourner un quelconque
rôle, cela voudrait dire que votre utilisateur n'est pas authentifié du tout.
Dans cet exemple, la classe entité AcmeUserBundle:User
définit une relation
« many-to-many » avec une classe entité AcmeUserBundle:Group
. Un utilisateur
peut être relié à plusieurs groupes et un groupe peut être composé d'un ou plusieurs
utilisateurs. Comme un groupe est aussi un rôle, la méthode précédente getRoles()
retourne maintenant la liste des groupes reliés:
// src/Acme/UserBundle/Entity/User.php namespace Acme\UserBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; // ... class User implements AdvancedUserInterface { /** * @ORM\ManyToMany(targetEntity="Group", inversedBy="users") * */ private $groups; public function __construct() { $this->groups = new ArrayCollection(); } // ... public function getRoles() { return $this->groups->toArray(); } }
La classe entité AcmeUserBundle:Group
définit trois champs de table (id
,
name
et role
). Le champ unique role
contient le nom du rôle utilisé
par la couche de sécurité de Symfony pour sécuriser des parties de l'application.
La chose la plus importante à noter est que la classe entité AcmeUserBundle:Group
implémente l'interface :class:`Symfony\\Component\\Security\\Core\\Role\\RoleInterface`
qui la force à avoir une méthode getRole()
:
// src/Acme/Bundle/UserBundle/Entity/Group.php namespace Acme\UserBundle\Entity; use Symfony\Component\Security\Core\Role\RoleInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="acme_groups") * @ORM\Entity() */ class Group implements RoleInterface { /** * @ORM\Column(name="id", type="integer") * @ORM\Id() * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(name="name", type="string", length=30) */ private $name; /** * @ORM\Column(name="role", type="string", length=20, unique=true) */ private $role; /** * @ORM\ManyToMany(targetEntity="User", mappedBy="groups") */ private $users; public function __construct() { $this->users = new ArrayCollection(); } // ... getters and setters for each property /** * @see RoleInterface */ public function getRole() { return $this->role; } }
Pour améliorer les performances et éviter le « lazy loading » de groupes lors de
la récupération d'un utilisateur depuis le fournisseur d'entité personnalisé, la
meilleure solution est d'effectuer une jointure avec la relation des groupes dans
la méthode UserRepository::loadUserByUsername()
. Cela va récupérer l'utilisateur
ainsi que ses rôles/groupes associés avec une requête unique:
// src/Acme/UserBundle/Entity/UserRepository.php namespace Acme\UserBundle\Entity; // ... class UserRepository extends EntityRepository implements UserProviderInterface { public function loadUserByUsername($username) { $q = $this ->createQueryBuilder('u') ->select('u, g') ->leftJoin('u.groups', 'g') ->where('u.username = :username OR u.email = :email') ->setParameter('username', $username) ->setParameter('email', $username) ->getQuery(); // ... } // ... }
La méthode QueryBuilder::leftJoin()
joint et cherche les groupes liés
depuis la classe modèle AcmeUserBundle:User
lorsqu'un utilisateur est
récupéré grâce à son adresse email ou à son nom d'utilisateur.