Admin
Admin
com/fr/courses/2078536-developpez-votre-site-web-avec-le-framework-
symfony2-ancienne-version/2082939-securite-et-gestion-des-utilisateurs
Authentification et autorisation
La sécurité sous Symfony2 est très poussée, vous pouvez la contrôler très finement, mais
surtout très facilement. Pour atteindre ce but, Symfony2 a bien séparé deux mécanismes
différents : l'authentification et l'autorisation. Prenez le temps de bien comprendre ces
deux notions pour bien attaquer la suite du cours.
L'authentification est le processus qui va définir qui vous êtes, en tant que visiteur. L'enjeu
est vraiment très simple : soit vous ne vous êtes pas identifié sur le site et vous êtes un
anonyme, soit vous vous êtes identifié (via le formulaire d'identification ou via un cookie
« Se souvenir de moi ») et vous êtes un membre du site. C'est ce que la procédure
d'authentification va déterminer. Ce qui gère l'authentification dans Symfony2 s'appelle
un firewall, ou un pare-feu en français.
Ainsi vous pourrez sécuriser des parties de votre site Internet juste en forçant le visiteur
à être un membre authentifié. Si le visiteur l'est, le firewall va le laisser passer, sinon il le
redirigera sur la page d'identification. Cela se fera donc dans les paramètres du firewall,
nous les verrons plus en détail par la suite.
L'autorisation
Par exemple, un membre identifié lambda aura accès à la liste de sujets d'un forum, mais
ne peut pas supprimer de sujet. Seuls les membres disposant des droits d'administrateur
le peuvent, c'est ce que l'access control va vérifier.
Exemples
Dans cet exemple, un visiteur anonyme souhaite accéder à la page/foo. Cette page ne
requiert pas de droits particuliers, donc tous ceux qui ont réussi à passer le firewall
peuvent y avoir accès. La figure suivante montre le processus.
Schéma du processus de sécurité
Sur ce schéma, vous distinguez bien le firewall d'un côté et l'access control (contrôle
d'accès) de l'autre. C'est très clair, mais reprenons-le ensemble pour bien comprendre :
Dans cet exemple, c'est le même visiteur anonyme qui veut accéder à la page/admin/foo.
Mais cette fois, la page/admin/foo requiert le rôle ROLE_ADMIN ; c'est un droit particulier,
nous le verrons plus loin. Notre visiteur va se faire refuser l'accès à la page, la figure
suivante montre comment.
Schéma du
processus de sécurité
Cet exemple est le même que précédemment, sauf que cette fois notre visiteur est
identifié, il s'appelle Ryan. Il n'est donc plus anonyme.
Schéma
du processus de sécurité
Nous allons construire pas à pas la sécurité de notre application. Cette section
commence donc par une approche théorique de la configuration de la sécurité avec
Symfony2 (notamment l'authentification), puis on mettra en place un formulaire de
connexion simple. On pourra ainsi s'identifier sur notre propre site, ce qui est plutôt
intéressant ! Par contre, les utilisateurs ne seront pas encore liés à la base de données,
on le verra un peu plus loin, avançons doucement.
# app/config/[Link]
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
providers:
in_memory:
memory:
users:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
access_control:
Section encoders
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
Un encodeur est un objet qui encode les mots de passe de vos utilisateurs. Cette
section de configuration permet donc de modifier l'encodeur utilisé pour vos
utilisateurs, et donc la façon dont sont encodés les mots de passe dans votre
application.
Vous l'avez deviné, ici l'encodeur utilisé plaintext n'encode en réalité rien du tout. Il
laisse en fait les mots de passe en clair, c'est pourquoi les mots de passe que nous
verrons dans une section juste en dessous sont en clair. Évidemment, nous définirons
par la suite un vrai encodeur, du type sha512, une méthode sûre !
Section role_hierarchy
security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
Cette section de la configuration dresse la hiérarchie des rôles. Ainsi, le rôle ROLE_USER
est compris dans le rôle ROLE_ADMIN. Cela signifie que si votre page requiert le rôle
ROLE_USER, et qu'un utilisateur disposant du rôle ROLE_ADMIN tente d'y accéder, il sera
autorisé, car en disposant du rôle d'administrateur, il dispose également du rôle
ROLE_USER.
Les noms des rôles n'ont pas d'importance, si ce n'est qu'ils doivent commencer par «
ROLE_ ».
Section providers
security:
providers:
in_memory:
memory:
users:
Pour l'instant vous pouvez le voir dans le fichier, un seul fournisseur est défini, nommé
in_memory (encore une fois, le nom est arbitraire). C'est un fournisseur assez
particulier dans le sens où les utilisateurs sont directement listés dans ce fichier de
configuration, il s'agit des utilisateurs « user » et « admin ». Vous l'aurez compris, c'est
un fournisseur pour faire du développement, pour tester la couche sécurité sans avoir
besoin d'une quelconque base de données derrière. Il faudra bien sûr le supprimer
par la suite.
Je vous rassure, il existe d'autres types de fournisseurs que celui-ci. On utilisera
notamment par la suite un fournisseur permettant de récupérer les utilisateurs dans la
base de données, il est déjà bien plus intéressant.
Section firewalls
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security : false
Comme on l'a vu précédemment, un firewall (ou pare-feu) cherche à vérifier que vous
êtes bien celui que vous prétendez être. Ici, seul le pare-feu dev est défini, nous avons
supprimé les autres pare-feu de démonstration. Ce pare-feu permet de désactiver la
sécurité sur certaines URL, on en reparle plus loin.
Section access_control
security:
access_control:
Soit ici depuis la configuration, en appliquant des règles sur des URL. On
sécurise ainsi un ensemble d'URL en une seule ligne, par exemple toutes celles
qui commencent par/admin.
Soit directement dans les contrôleurs, en appliquant des règles sur les
méthodes des contrôleurs. On peut ainsi appliquer des règles différentes selon
des paramètres, vous êtes très libres.
Ces deux moyens d'utiliser la même protection par rôle sont très complémentaires, et
offrent une flexibilité intéressante, on en reparle.
Il est temps de passer aux choses sérieuses, en mettant en place une authentification
pour notre application. Nous allons le faire en deux étapes. La première est la
construction d'un pare-feu, la deuxième est la construction d'un formulaire de
connexion. Commençons.
1. Créer le pare-feu
Commençons par créer un pare-feu simple, que nous appellerons main, comme ceci :
# app/config/[Link]
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: true
Dans les trois petites lignes que nous venons de rajouter :
Main est le nom du pare-feu. Il s'agit juste d'un identifiant unique, mettez en
réalité ce que vous voulez.
Pattern : ^/est un masque d'URL. Cela signifie que toutes les URL
commençant par « / » (c'est-à-dire notre site tout entier) sont protégées par ce
pare-feu. On dit qu'elles sont derrière le pare-feu main.
Anonymous : true accepte les utilisateurs anonymes. Nous protégerons nos
ressources grâce aux rôles.
Le pare-feu main recoupe les URL du pare-feu dev, c'est vrai. En fait, seul un unique
pare-feu peut agir sur une URL, et la règle d'attribution est la même que pour les routes
: premier arrivé, premier servi ! En l'occurrence, le pare-feu dev est défini avant notre
pare-feu main, donc une URL/css/…sera protégée par le pare-feu dev (car elle
correspond à son pattern). Ce pare-feu désactive totalement la sécurité, au final les
URL/css/…ne sont pas protégées du tout.
Si vous actualisez n'importe quelle page de votre site, vous pouvez maintenant voir
dans la barre d'outils en bas que vous êtes authentifié en tant qu'anonyme, comme
sur la figure suivante.
Je suis authentifié en tant qu'anonyme
Bon, votre pare-feu est maintenant créé, mais bien sûr il n'est pas complet, il manque
un élément indispensable pour le faire fonctionner : la méthode d'authentification. En
effet, votre pare-feu veut bien protéger vos URL, mais il faut lui dire comment vérifier
que vos visiteurs sont bien identifiés ! Et notamment, où trouver vos utilisateurs !
Nous allons faire simple pour la méthode d'authentification : un bon vieux formulaire
HTML. Pour configurer cela, c'est l'option form_login qu'il faut rajouter à notre pare-
feu :
# app/config/[Link]
security:
firewalls:
# ...
main:
pattern: ^/
anonymous: true
provider: in_memory
form_login:
login_path: login
check_path: login_check
logout:
path: logout
target: /platform
Expliquons-les quelques nouvelles lignes :
# app/config/[Link]
# ...
login:
pattern: /login
login_check:
pattern: /login_check
logout:
pattern : /logout
Comme vous pouvez le voir, on ne définit pas de contrôleur pour les routes
login_check et logout. Symfony2 va attraper tout seul les requêtes sur ces routes
(grâce au gestionnaire d'évènements, nous voyons cela dans un prochain chapitre).
Ce paragraphe n'est applicable que si vous ne disposez pas déjà d'un bundle
UserBundle.
Cela ne vous a pas échappé, j'ai défini le contrôleur à exécuter sur la route login
comme étant dans le bundle OCUserBundle. En effet, la gestion des utilisateurs sur un
site mérite amplement son propre bundle !
Le contrôleur Controller/[Link];
Son répertoire de tests Tests/Controller;
Son répertoire de vuesResources/views/Default;
Le fichier de routes Resources/config/[Link] ;
La ligne d'import du fichier de routes dans le fichier app/config/[Link].
Créer le formulaire de connexion
<?php
// src/OC/UserBundle/Controller/[Link];
namespace OC\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContext;
if ($this->get('[Link]')->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
return $this->redirectToRoute('oc_platform_accueil');
}
// Le service authentication_utils permet de récupérer le nom d'utilisateur
// et l'erreur dans le cas où le formulaire a déjà été soumis mais était invalide
$authenticationUtils = $this->get('security.authentication_utils');
));
}
Ne vous laissez pas impressionner par le contrôleur, de toute façon vous n'avez pas
à le modifier pour le moment. En réalité, il ne fait qu'afficher la vue du formulaire. Le
code au milieu n'est là que pour récupérer les erreurs d'une éventuelle soumission
précédente du formulaire. Rappelez-vous : c'est Symfony2 qui gère la soumission, et
lorsqu'il y a une erreur dans l'identification, il redirige le visiteur vers ce contrôleur, en
nous donnant heureusement l'erreur pour qu'on puisse lui afficher.
{# src/OC/UserBundle/Resources/views/Security/[Link] #}
{% extends "OCCoreBundle::[Link]" %}
{% block body %}
{% if error %}
<div class="alert alert-danger">{{ [Link] }}</div>
{% endif %}
<br />
</form>
{% endblock %}
La figure suivante montre le rendu du formulaire, accessible à l'adresse /login.
Le formulaire de connexion
Lorsque j'entre de faux identifiants, l'erreur générée est celle visible à la figure
suivante.
Mauvais identifiants
Enfin, lorsque j'entre les bons identifiants, la barre d'outils sur la page suivante
m'indique bien que je suis authentifié en tant qu'utilisateur « user », comme le montre
la figure suivante.
Voilà, notre formulaire de connexion est maintenant opérationnel. Vous trouverez plus
d'informations pour le personnaliser dans la documentation.
Une erreur bête est d'oublier de créer les routes login, login_check et logout. Ce
sont des routes obligatoires, et si vous les oubliez vous risquez de tomber sur des
erreurs 404 au milieu de votre processus d'authentification.
Vous devez vous assurer que l'URL du check_path (ici, /login_check) est bien
derrière le pare-feu que vous utilisez pour le formulaire de connexion (ici, main). En
effet, c'est la route qui permet l'authentification au pare-feu. Or, comme les pare-feu
ne partagent rien, si cette route n'appartient pas au pare-feu que vous voulez, vous
aurez droit à une belle erreur.
Dans notre cas, le pattern : ^/du pare-feu main prend bien l'URL /login_check, c'est
donc OK.
Cette erreur est vicieuse, car si vous sécurisez à tort l'URL /login, vous subirez une
redirection infinie. En effet, Symfony2 considère que vous n'avez pas accès à /login,
il vous redirige donc vers le formulaire pour vous authentifier, or il s'agit de la
page/login, or vous n'avez pas accès à/login, etc.
De plus, si vous souhaitez interdire les anonymes sur le pare-feu main, le problème se
pose également, car un nouvel arrivant sera forcément anonyme et ne pourra pas
accéder au formulaire de connexion. L'idée dans ce cas est de sortir le formulaire de
connexion (la page/login) du pare-feu main. En effet, c'est le check_path qui doit
obligatoirement appartenir au pare-feu, pas le formulaire en lui-même. Si vous
souhaitez interdire les anonymes sur votre site (et uniquement dans ce cas), vous
pouvez donc vous en sortir avec la configuration suivante :
# app/config/[Link]
# ...
firewalls:
main_login :
Pattern : ^/login$
anonymous: true # On autorise alors les anonymes sur ce pare-feu
main:
pattern: ^/
anonymous: false
# ...
En plaçant ce nouveau pare-feu avant notre pare-feu main, on sort le formulaire de
connexion du pare-feu sécurisé. Nos nouveaux arrivants auront donc une chance de
s'identifier !
Ce service dispose d'une méthode getToken (), qui permet de récupérer la session
de sécurité courante (à ne pas confondre avec la session classique, disponible elle via
$request->getSession ()). Ce token vaut null si vous êtes hors d'un pare-feu. Et si
vous êtes derrière un pare-feu, alors vous pouvez récupérer l'utilisateur courant grâce
à $token->getUser ().
<?php
// On récupère le service
$security = $container->get('[Link]');
// On récupère le token
// Sinon, c'est une instance de notre entité User, on peut l'utiliser normalement
$user->getUsername () ;
Comme vous pouvez le voir, il y a pas mal de vérifications à faire, suivant les différents
cas possibles. Heureusement, en pratique, le contrôleur dispose d'un raccourci
permettant d'automatiser cela, il s'agit de la méthode $this->getUser(). Cette
méthode retourne :
<?php
// Depuis un contrôleur
$user = $this->getUser () ;
} else {
}
Depuis une vue Twig
Vous avez accès plus facilement à l'utilisateur directement depuis Twig. Vous savez
que Twig dispose de quelques variables globales via la variable {{ app }} ; eh bien,
l'utilisateur courant en fait partie, via {{ [Link] }}:
Dans cette section, nous allons nous occuper de la deuxième couche de la sécurité :
l'autorisation. C'est une phase bien plus simple à gérer heureusement, il suffit juste
de demander tel(s) droit(s) à l'utilisateur courant (identifié ou non).
Tout d'abord, essayons d'imaginer les rôles dont on aura besoin dans notre application
de plateforme d'annonce. Je pense à :
Ce sont ces relations, et uniquement ces relations, que nous allons inscrire dans le
fichier [Link]. Voici donc comment décrire dans la configuration la hiérarchie
qu'on vient de définir :
# app/config/[Link]
security:
role_hierarchy :
J'insiste sur le fait qu'on définit ici uniquement la hiérarchie entre les rôles, et non
l'exhaustivité des rôles. Ainsi, on pourrait tout à fait avoir un rôle ROLE_TRUC dans notre
application, mais que les administrateurs n'héritent pas.
Tester les rôles de l'utilisateur
Il est temps maintenant de tester concrètement si l'utilisateur courant dispose de tel ou
tel rôle. Cela nous permettra de lui donner accès à la page, de lui afficher ou non un
certain lien, etc. Laissez libre cours à votre imagination.
Ce n'est pas le moyen le plus court, mais c'est celui par lequel passent les trois autres
méthodes. Il faut donc que je vous en parle en premier !
Depuis votre contrôleur ou n'importe quel autre service, il vous faut accéder au service
[Link] et appeler la méthode isGranted, tout simplement. Par exemple
dans notre contrôleur :
<?php
// src/OC/PlatformBundle/Controller/[Link]
namespace OC\PlatformBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class AdvertController extends Controller
if (!$this->get('[Link]')->isGranted('ROLE_AUTEUR')) {
}
C'est tout ! Vous pouvez aller sur /platform, mais impossible d'atteindre la page d'ajout
d'une annonce sur /platform/add, car vous ne disposez pas (encore !) du rôle
ROLE_AUTEUR, comme le montre la figure suivante.
L'accès est
interdit
Utiliser les annotations dans un contrôleur
<?php
// src/OC/PlatformBundle/Controller/[Link]
namespace OC\PlatformBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/**
* @Security("has_role('ROLE_AUTEUR')")
*/
// Dans cette méthode, vous êtes sûrs que l'utilisateur courant dispose du rôle
ROLE_AUTEUR
}
}
Et voilà ! Grâce à l'annotation @Security, on a sécurisé notre méthode en une seule
ligne, vraiment pratique.
La valeur de l'option par défaut de l'annotation est en fait une expression, dans laquelle
vous pouvez utiliser plusieurs variables et fonctions (dont has_role qu'on a utilisé ici).
Si vous voulez vérifier que l'utilisateur a deux rôles, vous pouvez faire comme ceci :
<?php
/**
* /
Le détail des variables et fonctions disponibles est dans la documentation.
Pour vérifier simplement que l'utilisateur est authentifié, et donc qu'il n'est pas
anonyme, vous pouvez utiliser le rôle spécial IS_AUTHENTICATED_REMEMBERED.
Pour cela, Twig dispose d'une fonction is_granted () qui est en réalité un raccourci
pour exécuter la méthode isGranted () du service [Link]. La voici en
application :
{% if is_granted('ROLE_AUTEUR') %}
{% endif %}
Utiliser les contrôles d'accès
La méthode de l'annotation permet de sécuriser une méthode de contrôleur. La
méthode avec Twig permet de sécuriser l'affichage. La méthode des contrôles d'accès
permet de sécuriser des URL. Elle se configure dans le fichier de configuration de la
sécurité, c'est la dernière section. Voici par exemple comment sécuriser tout un panel
d'administration (des pages dont l'URL commence par/admin) en une seule ligne :
# app/config/[Link]
security:
access_control:
C'est une méthode complémentaire des autres. Elle permet également de sécuriser
vos URL par IP ou par canal (http ou https), grâce à des options :
# app/config/[Link]
security:
access_control:
Pour tester les sécurités qu'on met en place, n'hésitez pas à charger vos pages avec
les deux utilisateurs « user » et « admin ». L'utilisateur admin ayant le rôle ROLE_ADMIN,
il a les droits pour ajouter une annonce et voir le lien d'ajout. Pour vous déconnecter
d'un utilisateur, allez sur /logout.
Heureusement il existe également une classe User qui implémente cette interface. Les
utilisateurs que nous avons actuellement sont des instances de cette classe.
Créons notre classe d'utilisateurs
En vue d'enregistrer nos utilisateurs en base de données, il nous faut créer notre
propre classe utilisateur, qui sera également une entité pour être persistée. Je vous
invite donc à générer directement une entité User au sein du bundle OCUserBundle,
grâce au générateur de Doctrine (php app/console doctrine:generate:entity), avec
les attributs minimum suivants (tirés de l'interface) :
<?php
namespace OC\UserBundle\Entity;
/**
* @ORM\Entity(repositoryClass="OC\UserBundle\Entity\UserRepository")
*/
class User
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
*/
private $username;
/**
*/
private $password;
/**
*/
private $salt;
/**
* @ORM\Column(name="roles", type="array")
*/
}
J'ai défini une valeur par défaut (array ()) à l'attribut $roles. J'ai également défini
l'attribut username comme étant unique, car c'est l'identifiant qu'utilise la couche
sécurité, il est donc obligatoire qu'il soit unique. Enfin, j'ai ajouté la méthode
eraseCredentials (), vide pour l'instant mais obligatoire de par l'interface suivante.
Et pour que Symfony2 l'accepte comme classe utilisateur de la couche sécurité, il faut
qu'on implémente l'interface UserInterface :
<?php
// src/OC/UserBundle/Entity/[Link]
use Symfony\Component\Security\Core\User\UserInterface;
// …
}
Et voilà, nous avons une classe prête à être utilisée !
Pour s'amuser avec notre nouvelle entitéUser, il faut créer quelques instances dans la
base de données. Réutilisons ici les fixtures, voici ce que je vous propose :
<?php
// src/OC/UserBundle/DataFixtures/ORM/[Link]
namespace OC\UserBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use OC\UserBundle\Entity\User;
// On crée l'utilisateur
$user->setUsername($name);
$user->setPassword($name);
$user->setSalt('');
$user->setRoles(array('ROLE_USER'));
// On le persiste
$manager->persist($user) ;
// On déclenche l'enregistrement
$manager->flush () ;
}
Exécutez cette fois la commande :
php app/console doctrine:fixtures:load
Et voilà, nous avons maintenant trois utilisateurs dans la base de données.
Ajoutez donc cet encodeur dans la configuration, juste en dessous de celui existant :
# app/config/[Link]
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
OC\UserBundle\Entity\User: plaintext
Définissons le fournisseur d'utilisateurs
On en a parlé plus haut, il faut définir un fournisseur (provider) pour que le pare-feu
puisse identifier et récupérer les utilisateurs.
Qu'est-ce qu'un fournisseur d'utilisateurs, concrètement ?
Un fournisseur
d'utilisateurs est une classe qui implémente l'interface
UserProviderInterface, qui contient juste trois méthodes :
Symfony2 dispose déjà de trois types de fournisseurs, qui implémentent tous l'interface
précédente évidemment, les voici :
memory utilise les utilisateurs définis dans la configuration, c'est celui qu'on a
utilisé jusqu'à maintenant ;
entity utilise de façon simple une entité pour fournir les utilisateurs, c'est celui
qu'on va utiliser ;
Id permet d'utiliser un service quelconque en tant que fournisseur, en précisant
le nom du service.
Créer notre fournisseur entity
Il est temps de créer le fournisseur entity pour notre entité User. Celui-ci existe déjà
dans Symfony2, nous n'avons donc pas de code à faire, juste un peu de configuration.
On va l'appeler « main », un nom arbitraire. Voici comment le déclarer :
# app/config/[Link]
security:
providers:
main:
entity:
class: OC\UserBundle\Entity\User
property: username
Il y a deux paramètres à préciser pour le fournisseur :
La classe à utiliser évidemment, il s'agit pour le fournisseur de savoir quel
repository Doctrine récupérer pour ensuite charger nos entités. Vous pouvez
également utiliser le nom logique de l'entité, ici OCUserBundle:User ;
L'attribut de la classe qui sert d'identifiant, on utilise username, donc on le lui dit.
Dans la configuration, faites bien la différence entre "main" et "entity" :
"main" est le nom du provider, totalement arbitraire. On aurait pu utiliser
"mon_super_provider", attention juste à bien utiliser le même dans la
configuration du pare-feu.
"entity" est le type de provider, c'est un nom fixe, définit dans symfony.
Dire au pare-feu d'utiliser le nouveau fournisseur
Maintenant que notre fournisseur existe, il faut demander au pare-feu de l'utiliser lui,
et non l'ancien fournisseur in_memory. Pour cela, modifions simplement la valeur du
paramètre provider, comme ceci :
# app/config/[Link]
security:
firewalls:
main:
pattern: ^/
anonymous: true
Je pourrais vous expliquer comment le faire, mais en réalité vous savez déjà le faire !
L'entité User que nous avons créée est une entité tout à fait comme les autres. À ce
stade du cours vous savez ajouter, modifier et supprimer des annonces, alors il en va
de même pour cette nouvelle entité qui représente vos utilisateurs.
Bref, faites-vous confiance, vous avez toutes les clés en main pour manipuler
entièrement vos utilisateurs.
Cependant, toutes les pages d'un espace membres sont assez classiques : inscription,
mot de passe perdu, modification du profil, etc. Tout cela est du déjà-vu. Et si c'est
déjà vu, il existe déjà certainement un bundle pour cela. Et je vous le confirme, il existe
même un excellent bundle, il s'agit de FOSUserBundle et je vous propose de l'installer
!
Utiliser FOSUserBundle
Comme vous avez pu le voir, la sécurité fait intervenir de nombreux acteurs et
demande pas mal de travail de mise en place. C'est normal, c'est un point sensible
d'un site internet. Heureusement, d'autres développeurs talentueux ont réussi à nous
faciliter la tâche en créant un bundle qui gère une partie de la sécurité !
Ce bundle s'appelle FOSUserBundle, il est très utilisé par la communauté Symfony2 car
vraiment bien fait, et surtout répondant à un besoin vraiment basique d'un site Internet
: l'authentification des membres.
Je vous propose donc d'installer ce bundle dans la suite de cette section. Cela n'est
en rien obligatoire, vous pouvez tout à fait continuer avec le User qu'on vient de
développer, cela fonctionne tout aussi bien !
Installation de FOSUserBundle
Télécharger le bundle
Mais pour ajouter ce bundle, vous l'avez compris, il faut utiliser Composer !
Commencez par déclarer cette nouvelle dépendance dans votre fichier [Link]:
// [Link]
// …
"require": {
// …
"friendsofsymfony/user-bundle": "dev-master"
}
// …
}
À l'heure où j'écris ces lignes, les mainteneurs de ce bundle n'ont pas sorti de version
2 stable du bundle. La branche 1.3.* est un peu vieille, je vous conseille donc d'utiliser
directement la branche master via dev-master.
Ensuite, il faut dire à Composer d'installer cette nouvelle dépendance :
php [Link] update friendsofsymfony/user-bundle
L'argument après la commande update permet de dire à Composer de ne mettre à
jour que cette dépendance. Ici, cela permet de ne mettre à jour que FOSUserBundle, et
pas les autres dépendances. C'est plus rapide, mais si vous vouliez tout mettre à jour,
supprimez simplement ce paramètre.
Activer le bundle
Si vos souvenirs sont bons, vous devriez savoir qu'un bundle ne s'active pas tout seul,
il faut aller l'enregistrer dans le noyau de Symfony2. Pour cela, ouvrez le fichier
app/[Link] pour enregistrer le bundle :
<?php
// app/[Link]
$bundles = array(
// …
new FOS\UserBundle\FOSUserBundle(),
);
}
C'est bon, le bundle est bien enregistré. Mais inutile d'essayer d'accéder à votre
application Symfony2 maintenant, elle ne marchera pas. Il faut en effet faire un peu de
configuration et de personnalisation avant de pouvoir tout remettre en marche.
<?php
// src/OC/UserBundle/[Link]
namespace OC\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
return 'FOSUserBundle';
}
Et c'est tout ! On a juste rajouté cette méthode getParent (), et Symfony2 va savoir
gérer le reste.
Si une vue du bundle A a le même nom qu'une vue du bundle B, c'est la vue du
bundle A qui sera utilisée lorsque vous faites "BundleB::[Link]",
alors que vous mentionnez bien "BundleB" dans le nom de la vue ;
Si un contrôleur du bundle A a le même nom qu'un contrôleur du bundle B, c'est
le contrôleur du bundle A qui sera utilisé lorsque vous faites "BundleB :
myController : myAction", alors que vous mentionnez bien "BundleB" dans le
nom du contrôleur.
Modifier notre entité User
Bien que nous ayons déjà créé une entité User, ce nouveau bundle en contient une
plus complète, qu'on va utiliser avec plaisir plutôt que de tout recoder nous-mêmes.
On va donc hériter l'entité User de FOSUserBundle depuis notre entité User de
notre OCUserBundle. Notre entité ne contiendra que les attributs que l'on souhaite
avoir et qui ne sont pas dans celle de FOSUserBundle. En fait, notre entité ne contient
plus grand-chose au final, voici ce que cela donne :
<?php
// src/OC/UserBundle/Entity/[Link]
namespace OC\UserBundle\Entity;
/**
* @ORM\Entity
*/
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
}
Plus besoin d'implémenter UserInterface, car on hérite de l'entité User du bundle
FOSUB, qui, elle, implémente cette interface. Pas besoin non plus d'écrire tous les
setters et getters, ils sont tous hérités, même le getter getId !
Alors c'est joli, mais pourquoi est-ce que l'on a fait cela ? En fait, le bundle
FOSUserBundle ne définit pas vraiment l'entité User, il définit une mapped superclass !
Un nom un peu barbare, juste pour dire que c'est une entité abstraite, et qu'il faut en
hériter pour en faire une vraie entité. C'est donc ce que nous venons juste de faire.
Cela permet en fait de garder la main sur notre entité. On peut ainsi lui ajouter des
attributs (selon vos besoins), en plus de ceux déjà définis. Pour information, les
attributs qui existent déjà sont :
Vous pouvez rajouter dès maintenant des attributs à votre entité User, comme vous
savez le faire depuis la partie Doctrine2.
Configurer le bundle
Ensuite, nous devons définir certains paramètres obligatoires au fonctionnement de
FOSUserBundle. Ouvrez votre [Link] et ajoutez la section suivante :
# app/config/[Link]
# …
fos_user:
Il faut maintenant mettre à jour la table des utilisateurs, vu les modifications que l'on
vient de faire. D'abord, allez la vider depuis phpMyAdmin, puis exécutez la commande
php app/console doctrine : schema : update --force. Et voilà, votre table est créée
!
On a fini d'initialiser le bundle. Bon, bien sûr pour l'instant Symfony2 ne l'utilise pas
encore, il manque un peu de configuration, attaquons-la.
L'encodeur
Il est temps d'utiliser un vrai encodeur pour nos utilisateurs, car il est bien sûr hors de
question de stocker leur mot de passe en clair ! On utilise couramment la méthode
sha512. Modifiez donc l'encodeur de notre classe comme ceci :
# app/config/[Link]
security:
encoders:
OC\UserBundle\Entity\User: sha512
Le fournisseur
Le bundle inclut son propre fournisseur en tant que service, qui utilise notre entité User
mais avec ses propres outils. Vous pouvez donc modifier notre fournisseur main
comme suit :
# app/config/[Link]
security:
# …
providers:
main:
id: fos_user.user_provider.username
Dans cette configuration, fos_user.user_provider. username est le nom du service
fourni par le bundle FOSUB.
Le pare-feu
Notre pare-feu était déjà pleinement opérationnel. Étant donné que nous n'avons pas
changé le nom du fournisseur associé, la configuration du pare-feu est déjà à jour.
Nous n'avons donc rien à modifier ici.
# app/config/[Link]
security:
# …
firewalls:
# … le pare-feu « dev »
main:
pattern: ^/
anonymous: true
provider: main
form_login:
login_path: login
check_path: login_check
logout:
path: logout
target: /platform
remember_me:
Pour tester à nouveau si tout fonctionne, il faut ajouter des utilisateurs à notre base de
données. Pour cela, on ne va pas réutiliser nos fixtures précédentes, mais on va
utiliser une commande très sympa proposée par FOSUserBundle. Exécutez la
commande suivante et laissez-vous guider :
php app/console fos:user:create
Vous l'aurez deviné, c'est une commande très pratique qui permet de créer des
utilisateurs facilement. Laissez-vous guider, elle vous demande le nom d'utilisateur,
l'e-mail et le mot de passe, et hop ! elle crée l'utilisateur. Vous pouvez aller vérifier le
résultat dans phpMyAdmin. Notez au passage que le mot de passe a bien été encodé,
en sha512 comme on l'a demandé.
FOSUserBundle offre bien plus que seulement de la sécurité. Du coup, maintenant que
la sécurité est bien configurée, passons au reste de la configuration du bundle.
En plus de gérer la sécurité, le bundle FOSUserBundle gère aussi les pages classiques
comme la page de connexion, celle d'inscription, etc. Pour toutes ces pages, il faut
évidemment enregistrer les routes correspondantes. Les développeurs du bundle ont
volontairement éclaté toutes les routes dans plusieurs fichiers pour pouvoir
personnaliser facilement toutes ces pages. Pour l'instant, on veut juste les rendre
disponibles, on les personnalisera plus tard. Ajoutez donc dans votre [Link] les
imports suivants à la suite du nôtre :
# app/config/[Link]
# …
fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/[Link]"
fos_user_profile:
resource: "@FOSUserBundle/Resources/config/routing/[Link]"
prefix: /profile
fos_user_register:
resource: "@FOSUserBundle/Resources/config/routing/[Link]"
prefix: /register
fos_user_resetting:
resource: "@FOSUserBundle/Resources/config/routing/[Link]"
prefix: /resetting
fos_user_change_password:
resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
prefix: /profile
Vous remarquez que les routes sont définies en XML et non en YML comme on en a
l'habitude dans ce cours. En effet, je vous en avais parlé tout au début, Symfony2
permet d'utiliser plusieurs méthodes pour les fichiers de configuration : YML, XML et
même PHP, au choix du développeur. Ouvrez ces fichiers de routes pour voir à quoi
ressemblent des routes en XML. C'est quand même moins lisible qu'en YML, c'est
pour cela qu'on a choisi YML au début.
Ouvrez vraiment ces fichiers pour connaître toutes les routes qu'ils contiennent. Vous
saurez ainsi faire des liens vers toutes les pages qu'offre le bundle : inscription, mot
de passe perdu, etc. Inutile de réinventer la roue ! Voici quand même un extrait de la
commande php app/console router: debug pour les routes qui concernent ce bundle
:
fos_user_security_login ANY ANY /login
# app/config/[Link]
security:
firewalls:
main:
pattern: ^/
anonymous: true
provider: main
form_login:
login_path: fos_user_security_login
check_path: fos_user_security_check
logout:
path: fos_user_security_logout
target: /platform
remember_me:
key: %secret%
Comme notre bundle OCUserBundle hérite de FOSUserBundle, c'est notre contrôleur et
donc notre vue qui sont utilisés sur la route login pour l'instant, car les noms que nous
avions utilisés sont les mêmes que ceux de FOSUserBundle. Étant donné que le
contrôleur de FOSUserBundle apporte un petit plus (protection CSRF notamment), je
vous propose de supprimer notre contrôleur SecurityController et notre vue
Security/[Link] pour laisser ceux de FOSUserBundle prendre la main.
Il reste quelques petits détails à gérer comme la page de login qui n'est pas la plus
sexy, sa traduction, et aussi un bouton « Déconnexion », parce que changer
manuellement l'adresse en/logout, c'est pas super user-friendly !
On va donc tout simplement le remplacer par une vue Twig qui va étendre notre layout.
Pour « remplacer » le layout du bundle, on va utiliser l'un des avantages d'avoir hérité
de ce bundle dans le nôtre, en créant une vue du même nom dans notre bundle.
Créez-donc la vue [Link] suivante :
{# src/OC/UserBundle/Resources/views/[Link] #}
{% extends "OCCoreBundle::[Link]" %}
{# Dans notre layout, il faut définir le block body #}
{% block body %}
{{ message|trans({}, 'FOSUserBundle') }}
</div>
{% endfor %}
{# On définit ce block, dans lequel vont venir s'insérer les autres vues du bundle #}
{% block fos_user_content %}
{% endblock fos_user_content %}
{% endblock %}
Pour créer ce layout je me suis simplement inspiré de celui fourni par FOSUserBundle,
en l'adaptant à notre cas.
Et voilà, si vous actualisez la page /login(après vous être déconnectés via /logout
évidemment), vous verrez que le formulaire de connexion est parfaitement intégré
dans notre design ! Vous pouvez également tester la page d'inscription sur /register,
qui est bien intégrée aussi.
Votre layout n'est pas pris en compte ? N'oubliez jamais d'exécuter la commande
php app/console cache : clear lorsque vous avez des erreurs qui vous étonnent !
Traduire les messages
FOSUB étant un bundle international, le texte est géré par le composant de traduction
de Symfony2. Par défaut, celui-ci est désactivé. Pour traduire le texte, il suffit donc de
l'activer (direction le fichier [Link]) et de décommenter une des premières lignes
dans framework :
# app/config/[Link]
framework:
{% if is_granted("IS_AUTHENTICATED_REMEMBERED") %}
{% else %}
{% endif %}
Adaptez et mettez ce code dans votre layout, effet garanti.
Nous allons voir les moyens pour manipuler vos utilisateurs au quotidien.
Si les utilisateurs sont gérés par FOSUserBundle, ils ne restent que des entités
Doctrine2 des plus classiques. Ainsi, vous pourriez très bien vous créer un repository
comme vous savez le faire. Cependant, profitons du fait que le bundle intègre un
UserManager (c'est une sorte de repository avancé). Ainsi, voici les principales
manipulations que vous pouvez faire avec :
<?php
// Dans un contrôleur :
// Pour récupérer le service UserManager du bundle
$userManager = $this->get('fos_user.user_manager');
$user->setEmail('cetemail@[Link]');
$userManager->deleteUser($user);
$users = $userManager->findUsers();
Si vous avez besoin de plus de fonctions, vous pouvez parfaitement faire un repository
personnel, et le récupérer comme d'habitude via $this->getDoctrine ()->
getManager () ->getRepository ('OCUserBundle : User'). Et si vous voulez en
savoir plus sur ce que fait le bundle dans les coulisses, n'hésitez pas à aller voir le
code des contrôleurs du bundle.
Pour conclure
Ce chapitre touche à sa fin. Vous avez maintenant tous les outils en main pour
construire votre espace membres, avec un système d'authentification performant et
sécurisé, et des accès limités pour vos pages suivant des droits précis.
Sachez que tout ceci n'est qu'une introduction à la sécurité sous Symfony2. Les
processus complets sont très puissants mais évidemment plus complexes. Si vous
souhaitez aller plus loin pour faire des opérations plus précises (authentification
Facebook, LDAP, etc.), n'hésitez pas à vous référer à la documentation officielle sur
la sécurité. Allez jeter un œil également à la documentation de FOSUserBundle, qui
explique comment personnaliser au maximum le bundle, ainsi que l'utilisation des
groupes.
Pour information, il existe également un système d'ACL, qui vous permet de définir
des droits bien plus finement que les rôles. Par exemple, pour autoriser l'édition d'une
annonce si on est admin ou si on en est l'auteur. Je ne traiterai pas ce point dans ce
cours, mais n'hésitez pas à vous référer à la documentation à ce sujet.
#