Dans ce chapitre, nous allons apprendre la sécurité avec Symfony2. C'est un chapitre assez technique, mais indispensable : à la fin nous aurons un espace membres fonctionnel et sécurisé !
Nous allons avancer en deux étapes : la première sera consacrée à la théorie de la sécurité sous Symfony2. Nécessaire, elle nous permettra d'aborder la deuxième étape : l'installation du bundle FOSUserBundle
, qui viendra compléter notre espace membres.
Bonne lecture !
- Authentification et autorisation
- Première approche de la sécurité
- Gestion des autorisations avec les rôles
- Utiliser des utilisateurs de la base de données
- Utiliser FOSUserBundle
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.
Les notions d'authentification et d'autorisation
L'authentification
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.
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
L'autorisation est le processus qui va déterminer si vous avez le droit d'accéder à la ressource (la page) demandée. Il agit donc après le firewall. Ce qui gère l'autorisation dans Symfony2 s'appelle l'access control.
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, ce que l'access control va vérifier.
Exemples
Pour bien comprendre la différence entre l'authentification et l'autorisation, je reprends ici les exemples de la documentation officielle, qui sont, je trouve, très intéressants et illustratifs. Dans ces exemples, vous distinguerez bien les différents acteurs de la sécurité.
Je suis anonyme, et je veux accéder à la page /foo qui ne requiert pas de droits
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.
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 :
- Le visiteur n'est pas identifié, il est anonyme, et tente d'accéder à la page
/foo
. - Le firewall est configuré de telle manière qu'il n'est pas nécessaire d'être identifié pour accéder à la page
/foo
. Il laisse donc passer notre visiteur anonyme. - Le contrôle d'accès regarde si la page
/foo
requiert des droits d'accès : il n'y en a pas. Il laisse donc passer notre visiteur, qui n'a aucun droit particulier. - Le visiteur a donc accès à la page
/foo
.
Je suis anonyme, et je veux accéder à la page /admin/foo qui requiert certains droits
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.
Voici le processus pas à pas :
- Le visiteur n'est pas identifié, il est toujours anonyme, et tente d'accéder à la page
/admin/foo
. - Le firewall est configuré de manière qu'il ne soit pas nécessaire d'être identifié pour accéder à la page
/admin/foo
. Il laisse donc passer notre visiteur. - Le contrôle d'accès regarde si la page
/admin/foo
requiert des droits d'accès : oui, il faut le rôleROLE_ADMIN
. Le visiteur n'a pas ce rôle, donc le contrôle d'accès lui interdit l'accès à la page/admin/foo
. - Le visiteur n'a donc pas accès à la page
/admin/foo
, et se fait rediriger sur la page d'identification.
Je suis identifié, et je veux accéder à la page /admin/foo qui requiert certains droits
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.
- Ryan s'identifie et il tente d'accéder à la page
/admin/foo
. D'abord, le firewall confirme l'authentification de Ryan (c'est son rôle !). Visiblement c'est bon, il laisse donc passer Ryan. - Le contrôle d'accès regarde si la page
/admin/foo
requiert des droits d'accès : oui, il faut le rôleROLE_ADMIN
, que Ryan n'a pas. Il interdit donc l'accès à la page/admin/foo
à Ryan. - Ryan n'a pas accès à la page
/admin/foo
non pas parce qu'il ne s'est pas identifié, mais parce que son compte utilisateur n'a pas les droits suffisants. Le contrôle d'accès lui affiche donc une page d'erreur lui disant qu'il n'a pas les droits suffisants.
Je suis identifié, et je veux accéder à la page /admin/foo qui requiert des droits que j'ai
Ici, nous sommes maintenant identifiés en tant qu'administrateur, on a donc le rôle ROLE_ADMIN
! Du coup, nous pouvons accéder à la page /admin/foo
, comme le montre la figure suivante.
- L'utilisateur admin s'identifie, et il tente d'accéder à la page
/admin/foo
. D'abord, le firewall confirme l'authentification d'admin. Ici aussi, c'est bon, il laisse donc passer admin. - Le contrôle d'accès regarde si la page
/admin/foo
requiert des droits d'accès : oui, il faut le rôleROLE_ADMIN
, qu'admin a bien. Il laisse donc passer l'utilisateur. - L'utilisateur admin a alors accès à la page
/admin/foo
, car il est identifié et il dispose des droits nécessaires.
Processus général
Lorsqu'un utilisateur tente d'accéder à une ressource protégée, le processus est finalement toujours le même, le voici :
- Un utilisateur veut accéder à une ressource protégée ;
- Le firewall redirige l'utilisateur au formulaire de connexion ;
- L'utilisateur soumet ses informations d'identification (par exemple login et mot de passe) ;
- Le firewall authentifie l'utilisateur ;
- L'utilisateur authentifié renvoie la requête initiale ;
- Le contrôle d'accès vérifie les droits de l'utilisateur, et autorise ou non l'accès à la ressource protégée.
Ces étapes sont simples, mais très flexibles. En effet, derrière le mot « authentification » se cache en pratique bien des méthodes : un formulaire de connexion classique, mais également l'authentification via Facebook, Google, etc., ou via les certificats X.509, etc. Bref, le processus reste toujours le même, mais les méthodes pour authentifier vos internautes sont nombreuses, et répondent à tous vos besoins. Et, surtout, elles n'ont pas d'impact sur le reste de votre code : qu'un utilisateur soit authentifié via Facebook ou un formulaire classique ne change rien à vos contrôleurs !
Première approche de la sécurité
Si les processus que nous venons de voir sont relativement simples, leur mise en place et leur configuration nécessitent un peu de travail.
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.
Le fichier de configuration de la sécurité
La sécurité étant un point important, elle a l'honneur d'avoir son propre fichier de configuration. Il s'agit du fichier security.yml
, situé dans le répertoire app/config
de votre application. Je vous propose déjà d'y faire un petit nettoyage : supprimez les deux sections login
et secured_area
sous la section firewalls
, elles concernent le bundle de démonstration que nous avons supprimé au début du cours. Votre fichier doit maintenant ressembler à ceci :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # app/config/security.yml jms_security_extra: secure_all_services: false expressions: true security: encoders: Symfony\Component\Security\Core\User\User: plaintext role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] providers: in_memory: memory: users: user: { password: userpass, roles: [ 'ROLE_USER' ] } admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false access_control: #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } |
Bien évidemment, rien de tout cela ne vous parle pour le moment. Rassurez-vous : à la fin du chapitre ce fichier ne vous fera plus peur. Pour le moment, décrivons rapidement chaque section de la configuration.
Section jms_security_extra
1 2 3 | jms_security_extra: secure_all_services: false expressions: true |
Cette section concerne le bundle JMSSecurityExtraBundle
, qui est livré par défaut avec Symfony2. Il apporte quelques petits plus à la sécurité, que nous verrons plus loin dans ce chapitre. Nous ne toucherons de toute façon pas à sa configuration, laissons cette section de côté pour l'instant.
Section encoders
1 2 3 | 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
1 2 3 4 | security: role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] |
La notion de « rôle » est au centre du processus d'autorisation. On assigne un ou plusieurs rôles à chaque utilisateur, et chaque ressource nécessite un ou plusieurs rôles. Ainsi, lorsqu'un utilisateur tente d'accéder à une ressource, le contrôleur d'accès vérifie s'il dispose du ou des rôles requis par la ressource. Si c'est le cas, l'accès est accordé. Sinon, l'accès est refusé.
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
1 2 3 4 5 6 7 | security: providers: in_memory: memory: users: user: { password: userpass, roles: [ 'ROLE_USER' ] } admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } |
Un provider est un fournisseur d'utilisateurs. Les firewalls s'adressent aux providers pour récupérer les utilisateurs pour les identifier.
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 de développement, pour tester la couche sécurité sans avoir besoin d'une quelconque base de données derrière.
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
1 2 3 4 5 | 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
1 2 3 | security: access_control: #- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } |
Comme on l'a vu, le contrôle d'accès (ou access control en anglais) va s'occuper de déterminer si le visiteur a les bons droits (rôles) pour accéder à la ressource demandée. Il y a différents moyens d'utiliser les contrôles d'accès :
- 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.
Mettre en place un pare-feu
Maintenant que nous avons survolé le fichier de configuration, vous avez une vue d'ensemble rapide de ce qu'il est possible de configurer. Parfait !
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 :
1 2 3 4 5 6 7 8 9 10 11 | # app/Config/security.yml 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-feumain
.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, un seul 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és en tant qu'anonyme, comme sur la figure suivante.
Authentifié en tant qu'anonyme ? C'est pas un peu bizarre ça ?
Hé, hé ! en effet. En fait les utilisateurs anonymes sont techniquement authentifiés. Mais ils restent des anonymes, et si nous mettions la valeur du paramètre anonymous
à false
, on serait bien refusés. Pour distinguer les anonymes authentifiés des vrais membres authentifiés, il faudra jouer sur les rôles, on en reparle plus loin, ne vous inquiétez pas.
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 !
Définir une méthode d'authentification pour le pare-feu
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # app/config/security.yml security: firewalls: # ... main: pattern: ^/ anonymous: true provider: in_memory form_login: login_path: login check_path: login_check logout: path: logout target: /blog |
Expliquons les quelques nouvelles lignes :
provider: in_memory
est le fournisseur d'utilisateurs pour ce pare-feu. Comme je vous l'ai mentionné précédemment, un pare-feu a besoin de savoir où trouver ses utilisateurs, cela se fait par le biais de ce paramètre. La valeurin_memory
correspond au fournisseur défini dans la sectionproviders
qu'on a vue précédemment.form_login
est la méthode d'authentification utilisée pour ce pare-feu. Elle correspond à la méthode classique, via un formulaire HTML. Ses options sont les suivantes :login_path: login
correspond à la route du formulaire de connexion. En effet, ce formulaire est bien disponible à une certaine adresse, il s'agit ici de la routelogin
, que nous définissons juste après.check_path: login_check
correspond à la route de validation du formulaire de connexion, c'est sur cette route que seront vérifiés les identifiants renseignés par l'utilisateur sur le formulaire précédent.
logout
rend possible la déconnexion. En effet, par défaut il est impossible de se déconnecter une fois authentifié. Ses options sont les suivantes :path
est le nom de la route à laquelle le visiteur doit aller pour être déconnecté. On va la définir plus loin.target
est l'URL vers laquelle sera redirigé le visiteur après sa déconnexion.
Je vous dois plus d'explications. Rappelez-vous, le processus est le suivant : lorsque le système de sécurité (ici, le pare-feu) initie le processus d'authentification, il va rediriger l'utilisateur sur le formulaire de connexion (la route login
). On va créer ce formulaire juste après, il devra envoyer les valeurs vers la route (ici, login_check
) qui va prendre en charge la soumission du formulaire.
Nous nous occupons du formulaire, mais c'est le système de sécurité de Symfony2 qui va s'occuper de la soumission de ce formulaire. Concrètement, nous allons définir un contrôleur à exécuter pour la route login
, mais pas pour la route login_check
! Symfony2 va attraper la requête de notre visiteur sur la route login_check
, et gérer lui-même l'authentification. En cas de succès, le visiteur sera authentifié. En cas d'échec, Symfony2 le renvoie vers notre formulaire de connexion.
Voici alors les trois routes à définir dans le fichier routing.yml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 | # app/config/routing.yml # ... login: pattern: /login defaults: { _controller: SdzUserBundle:Security: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).
Créer le bundle SdzUserBundle
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 SdzUserBundle
. En effet, la gestion des utilisateurs sur un site mérite amplement son propre bundle !
Je vous laisse générer ce bundle à l'aide de la commande suivante qu'on a déjà abordée :
1 | php app/console generate:bundle
|
Si vous mettez yes
pour importer automatiquement les routes du bundle généré, la commande va vous retourner ce message :
1 2 3 4 5 6 7 8 | Importing the bundle routing resource: FAILED The command was not able to configure everything automatically. You must do the following changes manually. Bundle SdzUserBundle is already imported. |
C'est parce qu'elle a détecté qu'on utilisait déjà une route vers ce bundle (celle qu'on vient de créer !), du coup elle n'a pas osé réimporter les routes du bundle. C'est de toute façon totalement inutile, pour l'instant on n'a pas de route dans ce bundle.
Avant de continuer, je vous propose un petit nettoyage, car le générateur a tendance à trop en faire. Vous pouvez donc supprimer allègrement :
- Le contrôleur
Controller/DefaultController.php
; - Son répertoire de tests
Tests/Controller
; - Son répertoire de vues
Resources/views/Default
; - Le fichier de routes
Resources/config/routing.yml
.
Créer le formulaire de connexion
Il s'agit maintenant de créer le formulaire de connexion, disponible sur la route login
, soit l'URL /login
. Commençons par le contrôleur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <?php // src/Sdz/UserBundle/Controller/SecurityController.php; namespace Sdz\UserBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Security\Core\SecurityContext; class SecurityController extends Controller { public function loginAction() { // Si le visiteur est déjà identifié, on le redirige vers l'accueil if ($this->get('security.context')->isGranted('IS_AUTHENTICATED_REMEMBERED')) { return $this->redirect($this->generateUrl('sdzblog_accueil')); } $request = $this->getRequest(); $session = $request->getSession(); // On vérifie s'il y a des erreurs d'une précédente soumission du formulaire if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR); } else { $error = $session->get(SecurityContext::AUTHENTICATION_ERROR); $session->remove(SecurityContext::AUTHENTICATION_ERROR); } return $this->render('SdzUserBundle:Security:login.html.twig', array( // Valeur du précédent nom d'utilisateur entré par l'internaute 'last_username' => $session->get(SecurityContext::LAST_USERNAME), 'error' => $error, )); } } |
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.
La vue pourrait être la suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | {# src/Sdz/UserBundle/Resources/views/Security/login.html.twig #} {% extends "::layout.html.twig" %} {% block body %} {# S'il y a une erreur, on l'affiche dans un joli cadre #} {% if error %} <div class="alert alert-error">{{ error.message }}</div> {% endif %} {# Le formulaire, avec URL de soumission vers la route « login_check » comme on l'a vu #} <form action="{{ path('login_check') }}" method="post"> <label for="username">Login :</label> <input type="text" id="username" name="_username" value="{{ last_username }}" /> <label for="password">Mot de passe :</label> <input type="password" id="password" name="_password" /> <br /> <input type="submit" value="Connexion" /> </form> {% endblock %} |
La figure suivante montre le rendu du formulaire, accessible à l'adresse /login
.
Lorsque j'entre de faux identifiants, l'erreur générée est celle visible à la figure suivante.
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.
Mais quels sont les bons identifiants ?
Il fallait lire attentivement le fichier de configuration qu'on a parcouru précédemment. Rappelez-vous, on a défini le fournisseur de notre pare-feu à in_memory
, qui est défini quelques lignes plus haut dans le fichier de configuration. Ce fournisseur est particulier, dans le sens où il lit les utilisateurs directement dans cette configuration. On a donc deux utilisateurs possibles : « user » et « admin », avec pour mot de passe respectivement « userpass » et « adminpass ».
Voilà, notre formulaire de connexion est maintenant opérationnel. Vous trouverez plus d'informations pour le personnaliser dans la documentation.
Les erreurs courantes
Il y a quelques pièges à connaître quand vous travaillerez plus avec la sécurité, en voici quelques-uns.
Ne pas oublier la définition des routes
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.
Les pare-feu ne partagent pas
Si vous utilisez plusieurs pare-feu, sachez qu'ils ne partagent rien les uns avec les autres. Ainsi, si vous êtes authentifiés sur l'un, vous ne le serez pas forcément sur l'autre, et inversement. Cela permet d’accroître la sécurité lors d'un paramétrage complexe.
Bien mettre /login_check derrière le pare-feu
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.
Ne pas sécuriser le formulaire de connexion
En effet, si le formulaire est sécurisé, comment les nouveaux arrivants vont-ils pouvoir s'authentifier ? En l'occurrence, il faut faire attention que la page /login
ne requière aucun rôle, on fera attention à cela lorsqu'on va définir les autorisations.
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 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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # app/config/security.yml # ... firewalls: # On crée un pare-feu uniquement pour le formulaire main_login: # Cette expression régulière permet de prendre /login (mais pas /login_check !) 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 !
Récupérer l'utilisateur courant
Pour récupérer les informations sur l'utilisateur courant, qu'il soit anonyme ou non, il faut utiliser le service security.context
.
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()
.
Depuis le contrôleur ou un service
Voici concrètement comment l'utiliser :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <?php // On récupère le service $security = $container->get('security.context'); // On récupère le token $token = $security->getToken(); // Si la requête courante n'est pas derrière un pare-feu, $token est null // Sinon, on récupère l'utilisateur $user = $token->getUser(); // Si l'utilisateur courant est anonyme, $user vaut « anon. » // 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 :
null
si la requête n'est pas derrière un pare-feu, ou si l'utilisateur courant est anonyme ;- Une instance de
User
le reste du temps (utilisateur authentifié derrière un pare-feu et non-anonyme).
Du coup, voici le code simplifié depuis un contrôleur :
1 2 3 4 5 6 7 8 9 10 | <?php // Depuis un contrôleur $user = $this->getUser(); if (null === $user) { // Ici, l'utilisateur est anonyme ou l'URL n'est pas derrière un pare-feu } else { // Ici, $user est une instance de notre classe User } |
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 {{ app.user }}
:
1 | Bonjour {{ app.user.username }} - {{ app.user.email }} |
Au même titre que dans un contrôleur, attention à ne pas utiliser {{ app.user }}
lorsque l'utilisateur n'est pas authentifié, car il vaut null
.
Gestion des autorisations avec les rôles
La section précédente nous a amenés à réaliser une authentification opérationnelle. Vous avez un pare-feu, une méthode d'authentification par formulaire HTML, et deux utilisateurs. La couche authentification est complète !
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).
Définition des rôles
Rappelez-vous, on a croisé les rôles dans le fichier security.yml
. La notion de rôle et autorisation est très simple : pour limiter l'accès à certaines pages, on va se baser sur les rôles de l'utilisateur. Ainsi, limiter l'accès au panel d'administration revient à limiter cet accès aux utilisateurs disposant du rôle ROLE_ADMIN
(par exemple).
Tout d'abord, essayons d'imaginer les rôles dont on aura besoin dans notre application de blog. Je pense à :
ROLE_AUTEUR
: pour ceux qui ont le droit d'écrire des articles ;ROLE_MODERATEUR
: pour ceux qui peuvent modérer les commentaires ;ROLE_ADMIN
: pour ceux qui peuvent tout faire.
Maintenant l'idée est de créer une hiérarchie entre ces rôles. On va dire que les auteurs et les modérateurs sont bien différents, et que les admins ont les droits cumulés des auteurs et des modérateurs. Ainsi, pour limiter l'accès à certaines pages, on ne va pas faire « si l'utilisateur a ROLE_AUTEUR
ou s'il a ROLE_ADMIN
, alors il peut écrire un article ». Grâce à la définition de la hiérarchie, on peut faire simplement « si l'utilisateur a ROLE_AUTEUR
». Car un utilisateur qui dispose de ROLE_ADMIN
dispose également de ROLE_AUTEUR
, c'est une inclusion.
Ce sont ces relations, et uniquement ces relations, que nous allons inscrire dans le fichier security.yml
. Voici donc comment décrire dans la configuration la hiérarchie qu'on vient de définir :
1 2 3 4 5 6 | # app/config/security.yml security: role_hierarchy: ROLE_ADMIN: [ROLE_AUTEUR, ROLE_MODERATEUR] # Un admin hérite des droits d'auteur et de modérateur ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] # On garde ce rôle superadmin, il nous resservira par la suite |
Remarquez que je n'ai pas utilisé le rôle ROLE_USER
, qui n'est pas toujours utile. Avec cette hiérarchie, voici des exemples de tests que l'on peut faire :
- Si l'utilisateur a le rôle
ROLE_AUTEUR
, alors il peut écrire un article. Les auteurs et les admins peuvent donc le faire. - Si l'utilisateur a le rôle
ROLE_ADMIN
, alors il peut supprimer un article. Seuls les admins peuvent donc le faire.
Tous ces tests nous permettront de limiter l'accès à nos différentes pages.
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.
Il existe quatre méthodes pour faire ce test : les annotations, le service security.context
, Twig, et les contrôles d'accès. Ce sont quatre façons de faire exactement la même chose.
Utiliser directement le service security.context
Ce n'est pas le moyen le plus court, mais c'est celui par lequel passent les deux 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 security.context
et appeler la méthode isGranted
, tout simplement. Par exemple dans notre contrôleur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php namespace Sdz\BlogBundle\Controller; // Pensez à rajouter ce use pour l'exception qu'on utilise use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; // … class BlogController extends Controller { public function ajouterAction($form) { // On teste que l'utilisateur dispose bien du rôle ROLE_AUTEUR if (!$this->get('security.context')->isGranted('ROLE_AUTEUR')) { // Sinon on déclenche une exception « Accès interdit » throw new AccessDeniedHttpException('Accès limité aux auteurs'); } // … Ici le code d'ajout d'un article qu'on a déjà fait } // … } |
C'est tout ! Vous pouvez aller sur /blog
, mais impossible d'atteindre la page d'ajout d'un article sur /blog/ajouter
, car vous ne disposez pas (encore !) du rôle ROLE_AUTEUR
, comme le montre la figure suivante.
Utiliser les annotations dans un contrôleur
Pour faire exactement ce qu'on vient de faire avec le service security.context
, il existe un moyen bien plus rapide et joli : les annotations ! C'est ici qu'intervient le bundle JMSSecurityExtraBundle, présent et activé par défaut avec Symfony2. Pas besoin d'explication, c'est vraiment simple ; regardez le code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <?php // src/Sdz/BlogBundle/Controller/BlogController.php namespace Sdz\BlogBundle\Controller; // Plus besoin de rajouter le use de l'exception dans ce cas // Mais par contre il faut le use pour les annotations du bundle : use JMS\SecurityExtraBundle\Annotation\Secure; // … class BlogController extends Controller { /** * @Secure(roles="ROLE_AUTEUR") */ public function ajouterAction() { // Plus besoin du if avec le security.context, l'annotation s'occupe de tout ! // Dans cette méthode, vous êtes sûrs que l'utilisateur courant dispose du rôle ROLE_AUTEUR // … Ici le code d'ajout d'un article qu'on a déjà fait } // … } |
Et voilà ! Grâce à l'annotation @Secure
, on a sécurisé notre méthode en une seule ligne, vraiment pratique. Sachez que vous pouvez demander plusieurs rôles en même temps, en faisant @Secure(roles="ROLE_AUTEUR, ROLE_MODERATEUR")
, qui demandera le rôle ROLE_AUTEUR
et le rôle ROLE_MODERATEUR
(ce n'est pas un « ou » !).
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
.
Sachez qu'il existe d'autres vérifications possibles avec l'annotation @Secure
, je vous invite à jeter un œil à la documentation de JMSSecurityExtraBundle
.
Depuis une vue Twig
Cette méthode est très pratique pour afficher du contenu différent selon les rôles de vos utilisateurs. Typiquement, le lien pour ajouter un article ne doit être visible que pour les membres qui disposent du rôle ROLE_AUTEUR
(car c'est la contrainte que nous avons mise sur la méthode ajouterAction()
).
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 security.context
. La voici en application :
1 2 3 4 5 6 7 8 9 10 11 | {# app/Resources/views/layout.html.twig #} {# ... #} {# On n'affiche le lien « Ajouter un article » qu'aux auteurs (et admins, qui héritent du rôle auteur) #} {% if is_granted('ROLE_AUTEUR') %} <a href="{{ path('sdzblog_ajouter') }}">Ajouter un article</a> {% 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 :
1 2 3 4 5 | # app/config/security.yml security: access_control: - { path: ^/admin, roles: ROLE_ADMIN } |
Ainsi, toutes les URL qui correspondent au path
(ici, toutes celles qui commencent par /admin
) requièrent le rôle ROLE_ADMIN
.
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 :
1 2 3 4 5 | # app/config/security.yml security: access_control: - { path: ^/admin, roles: ROLE_ADMIN, ip: 127.0.0.1, requires_channel: https } |
Pour conclure sur les méthodes de sécurisation
Symfony2 offre plusieurs moyens de sécuriser vos ressources (méthode de contrôleur, affichage, URL). N'hésitez pas à vous servir de la méthode la plus appropriée pour chacun de vos besoins. C'est la complémentarité des méthodes qui fait l'efficacité de la sécurité avec Symfony2.
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 un article et voir le lien d'ajout. Pour vous déconnecter d'un utilisateur, allez sur /logout
.
Utiliser des utilisateurs de la base de données
Pour l'instant, nous n'avons fait qu'utiliser les deux pauvres utilisateurs définis dans le fichier de configuration. C'était pratique pour faire nos premiers tests, car ils ne nécessitent aucun paramétrage particulier. Mais maintenant, passons à la vitesse supérieure et enregistrons nos utilisateurs en base de données !
Qui sont les utilisateurs ?
Dans Symfony2, un utilisateur est un objet qui implémente l'interface UserInterface, c'est tout. N'hésitez pas à aller voir à quoi ressemble cette interface, il n'y a en fait que cinq méthodes obligatoires, ce n'est pas grand-chose.
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 SdzUserBundle
, grâce au générateur de Doctrine (php app/console doctrine:generate:entity
), avec les attributs minimum suivants (tirés de l'interface) :
username
: c'est l'identifiant de l'utilisateur au sein de la couche sécurité. Cela ne nous empêchera pas d'utiliser également un id numérique pour notre entité, c'est plus simple pour nous ;password
: le mot de passe ;salt
: le sel, pour encoder le mot de passe, on en reparle plus loin ;roles
: un tableau (attention à bien le définir comme tel lors de la génération) contenant les rôles de l'utilisateur.
Voici la classe que j'obtiens :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | <?php // src/Sdz/UserBundle/Entity/User.php namespace Sdz\UserBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="Sdz\UserBundle\Entity\UserRepository") */ class User { /** * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(name="username", type="string", length=255, unique=true) */ private $username; /** * @ORM\Column(name="password", type="string", length=255) */ private $password; /** * @ORM\Column(name="salt", type="string", length=255) */ private $salt; /** * @ORM\Column(name="roles", type="array") */ private $roles; public function __construct() { $this->roles = array(); } public function getId() { return $this->id; } public function setUsername($username) { $this->username = $username; return $this; } public function getUsername() { return $this->username; } public function setPassword($password) { $this->password = $password; return $this; } public function getPassword() { return $this->password; } public function setSalt($salt) { $this->salt = $salt; return $this; } public function getSalt() { return $this->salt; } public function setRoles(array $roles) { $this->roles = $roles; return $this; } public function getRoles() { return $this->roles; } public function eraseCredentials() { } } |
J'ai rajouté un constructeur pour définir 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
:
1 2 3 4 5 6 7 8 9 | <?php // src/Sdz/UserBundle/Entity/User.php use Symfony\Component\Security\Core\User\UserInterface; class User implements UserInterface { // … } |
Et voilà, nous avons une classe prête à être utilisée !
Et bien sûr, exécutez un petit php app/console doctrine:schema:update
pour mettre à jour la base de données avec cette nouvelle entité.
Créons quelques utilisateurs de test
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <?php // src/Sdz/UserBundle/DataFixtures/ORM/Users.php namespace Sdz\UserBundle\DataFixtures\ORM; use Doctrine\Common\DataFixtures\FixtureInterface; use Doctrine\Common\Persistence\ObjectManager; use Sdz\UserBundle\Entity\User; class Users implements FixtureInterface { public function load(ObjectManager $manager) { // Les noms d'utilisateurs à créer $noms = array('winzou', 'John', 'Talus'); foreach ($noms as $i => $nom) { // On crée l'utilisateur $users[$i] = new User; // Le nom d'utilisateur et le mot de passe sont identiques $users[$i]->setUsername($nom); $users[$i]->setPassword($nom); // Le sel et les rôles sont vides pour l'instant $users[$i]->setSalt(''); $users[$i]->setRoles(array()); // On le persiste $manager->persist($users[$i]); } // On déclenche l'enregistrement $manager->flush(); } } |
Exécutez cette fois la commande :
1 | php app/console doctrine:fixtures:load
|
Et voilà, nous avons maintenant trois utilisateurs dans la base de données.
Définissons l'encodeur pour notre nouvelle classe d'utilisateurs
Ce n'est pas un piège mais presque, rappelez-vous l'encodeur défini pour nos précédents utilisateurs spécifiait la classe User
utilisée. Or maintenant nous allons nous servir d'une autre classe, il s'agit de Sdz\UserBundle\Entity\User
. Il est donc obligatoire de définir quel encodeur utiliser pour notre nouvelle classe. Comme nous avons mis les mots de passe en clair dans les fixtures, nous devons également utiliser l'encodeur plaintext
, qui n'encode pas les mots de passe mais les laisse en clair, c'est plus simple pour nos tests.
Ajoutez donc cet encodeur dans la configuration, juste en dessous de celui existant :
1 2 3 4 5 6 | # app/config/security.yml security: encoders: Symfony\Component\Security\Core\User\User: plaintext Sdz\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 :
loadUserByUsername($username)
, qui charge un utilisateur à partir d'un nom d'utilisateur ;refreshUser($user)
, qui rafraîchit un utilisateur avec les valeurs d'origine ;supportsClass()
, qui détermine quelle classe d'utilisateurs gère le fournisseur.
Vous pouvez le constater, un fournisseur ne fait pas grand-chose, à part charger ou rafraîchir les utilisateurs.
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é. 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 :
1 2 3 4 5 6 7 8 | # app/config/security.yml security: providers: # … vous pouvez supprimer le fournisseur « in_memory » # Et voici notre nouveau fournisseur : main: entity: { class: Sdz\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 ;
- L'attribut de la classe qui sert d'identifiant, on utilise
username
, donc on le lui dit.
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 :
1 2 3 4 5 6 7 8 9 | # app/config/security.yml security: firewalls: main: pattern: ^/ anonymous: true provider: main # On change cette valeur # … reste de la configuration du pare-feu |
Vous trouverez encore plus d'informations sur ce type de fournisseur dans la documentation.
Manipuler vos utilisateurs
La couche sécurité est maintenant pleinement opérationnelle et utilise des utilisateurs stockés en base de données. C'est parfait !
Vous voulez faire un formulaire d'inscription ? Modifier vos utilisateurs ? Changer leurs rôles ?
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 articles de blog, 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
Le bundle FOSUserBundle
est hébergé sur GitHub, comme beaucoup de bundles et projets Symfony2. Sa page est ici : https://github.com/FriendsOfSymfony/FOSUserBundle
.
Mais pour ajouter ce bundle, vous l'avez compris, il faut utiliser Composer ! Commencez par déclarer cette nouvelle dépendance dans votre fichier composer.json
:
1 2 3 4 5 6 7 8 9 10 11 12 | // composer.json { // … "require": { // … "friendsofsymfony/user-bundle": "dev-master" } // … } |
Ensuite, il faut dire à Composer d'installer cette nouvelle dépendance :
1 | php composer.phar 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/AppKernel.php
pour enregistrer le bundle :
1 2 3 4 5 6 7 8 9 10 | <?php // app/AppKernel.php public function registerBundles() { $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.
Hériter FOSUserBundle depuis notre SdzUserBundle
FOSUserBundle
est un bundle générique évidemment, car il doit pouvoir s'adapter à tout type d'utilisateur de n'importe quel site internet. Vous imaginez bien que, du coup, ce n'est pas un bundle prêt à l'emploi directement après son installation ! Il faut donc s'atteler à le personnaliser afin de faire correspondre le bundle à nos besoins. Cette personnalisation passe par l'héritage de bundle.
C'est une fonctionnalité intéressante qui va nous permettre de personnaliser facilement et proprement le bundle que l'on vient d'installer. L'héritage de bundle est même très simple à réaliser. Prenez le fichier SdzUserBundle.php
qui représente notre bundle, et modifiez-le comme suit :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php // src/Sdz/UserBundle/SdzUserBundle.php namespace Sdz\UserBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class SdzUserBundle extends Bundle { public function getParent() { return 'FOSUserBundle'; } } |
Et c'est tout ! On a juste rajouté cette méthode getParent()
, et Symfony2 va savoir gérer le reste.
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. Ici on va hériter de l'entité User
du bundle, depuis notre entité User
de notre bundle. En fait, notre entité ne contient plus grand-chose au final, voici ce que cela donne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php // src/Sdz/UserBundle/Entity/User.php namespace Sdz\UserBundle\Entity; use FOS\UserBundle\Entity\User as BaseUser; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="sdz_user") */ class User extends BaseUser { /** * @ORM\Id * @ORM\Column(type="integer") * @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.
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 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 :
username
: nom d'utilisateur avec lequel l'utilisateur va s'identifier ;email
: l'adresse e-mail ;enabled
:true
oufalse
suivant que l'inscription de l'utilisateur a été validée ou non (dans le cas d'une confirmation par e-mail par exemple) ;password
: le mot de passe de l'utilisateur ;lastLogin
: la date de la dernière connexion ;locked
: si vous voulez désactiver des comptes ;expired
: si vous voulez que les comptes expirent au-delà d'une certaine durée.
Je vous en passe certains qui sont plus à un usage interne. Sachez tout de même que vous pouvez tous les retrouver dans la définition de la mapped superclass. C'est un fichier de mapping XML, l'équivalent des annotations qu'on utilise de notre côté.
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 config.yml
et ajoutez la section suivante :
1 2 3 4 5 6 7 8 | # app/config/config.yml # … fos_user: db_driver: orm # Le type de BDD à utiliser, nous utilisons l'ORM Doctrine depuis le début firewall_name: main # Le nom du firewall derrière lequel on utilisera ces utilisateurs user_class: Sdz\UserBundle\Entity\User # La classe de l'entité User que nous utilisons |
Et voilà, on a bien installé FOSUserBundle
! Avant d'aller plus loin, créons la table User
et ajoutons quelques membres pour les tests.
Mise à jour de la table User
Il faut 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.
Configuration de la sécurité pour utiliser le bundle
Maintenant on va reprendre notre configuration de la sécurité, pour utiliser tous les outils fournis par le bundle dès que l'on peut. Reprenez le security.yml
sous la main, et c'est parti !
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 :
1 2 3 4 5 | # app/config/security.yml security: encoders: Sdz\UserBundle\Entity\User: sha512 |
Le fournisseur
Le bundle inclut son propre fournisseur, qui utilise notre entité User
mais avec ses propres outils. Vous pouvez donc modifier notre fournisseur main
comme suit :
1 2 3 4 5 6 7 8 9 | # app/config/security.yml security: # … providers: main: id: fos_user.user_provider.username |
Dans cette configuration, fos_user.user_manager
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.
On va juste en profiter pour activer la possibilité de « Se souvenir de moi » à la connexion. Cela permet aux utilisateurs de ne pas s'authentifier manuellement à chaque fois qu'ils accèdent à notre site. Ajoutez donc l'option remember_me
dans la configuration. Voici ce que cela donne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # app/config/security.yml security: # … firewalls: # … le pare-feu « dev » # Firewall principal pour le reste de notre site main: pattern: ^/ anonymous: true provider: main form_login: login_path: login check_path: login_check logout: path: logout target: /blog remember_me: key: %secret% # %secret% est un paramètre de parameters.yml |
J'ai juste ajouté le dernier paramètre remember_me
.
Configuration de la sécurité : check !
Et voilà, votre site est prêt à être sécurisé ! En effet, on a fini de configurer la sécurité pour utiliser tout ce qu'offre le bundle à ce niveau.
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 :
1 | 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.
Configuration du bundle FOSUserBundle
Configuration des routes
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 routing.yml
les imports suivants à la suite du nôtre :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # app/config/routing.yml # … fos_user_security: resource: "@FOSUserBundle/Resources/config/routing/security.xml" fos_user_profile: resource: "@FOSUserBundle/Resources/config/routing/profile.xml" prefix: /profile fos_user_register: resource: "@FOSUserBundle/Resources/config/routing/registration.xml" prefix: /register fos_user_resetting: resource: "@FOSUserBundle/Resources/config/routing/resetting.xml" 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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | fos_user_security_login ANY ANY /login fos_user_security_check ANY ANY /login_check fos_user_security_logout ANY ANY /logout fos_user_profile_show GET ANY /profile/ fos_user_profile_edit ANY ANY /profile/edit fos_user_registration_register ANY ANY /register/ fos_user_registration_check_email GET ANY /register/check-email fos_user_registration_confirm GET ANY /register/confirm/{token} fos_user_registration_confirmed GET ANY /register/confirmed fos_user_resetting_request GET ANY /resetting/request fos_user_resetting_send_email POST ANY /resetting/send-email fos_user_resetting_check_email GET ANY /resetting/check-email fos_user_resetting_reset GET|POST ANY /resetting/reset/{token} fos_user_change_password GET|POST ANY /profile/change-password |
Vous notez que le bundle définit également les routes de sécurité /login
et autres. Du coup, je vous propose de laisser le bundle gérer cela, supprimez donc les trois routes login
, login_check
et logout
qu'on avait déjà définies et qui ne servent plus. De plus, il faut adapter la configuration du pare-feu, car le nom de ces routes a changé, voici ce que cela donne :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # app/config/security.yml 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: /blog remember_me: key: %secret% # %secret% est un paramètre de parameters.yml |
Comme notre bundle SdzUserBundle
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/login.html.twig
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 !
Personnalisation esthétique du bundle
Heureusement tout cela est assez simple.
Attention, la personnalisation esthétique que nous allons faire ne concerne en rien la couche sécurité à proprement parler. Soyez bien conscients de la différence !
Intégrer les pages du bundle dans notre layout
FOSUserBundle
utilise un layout volontairement simpliste, parce qu'il a vocation à être remplacé par le nôtre. Le layout actuel est le suivant : https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Resources/views/layout.html.twig
On va donc tout simplement le remplacer par une vue Twig qui va étendre notre layout (qui est dans app/Resources/views/layout.html.twig
, rappelez-vous). 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 layout.html.twig
suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | {# src/Sdz/UserBundle/Resources/views/layout.html.twig #} {# On étend notre layout #} {% extends "::layout.html.twig" %} {# Dans notre layout, il faut définir le block body #} {% block body %} {# On affiche les messages flash que définissent les contrôleurs du bundle #} {% for key, message in app.session.flashbag.all() %} <div class="alert alert-{{ key }}"> {{ 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 juste à 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 config.yml
) et de décommenter une des premières lignes dans framework
:
1 2 3 4 | # app/config/config.yml framework: translator: { fallback: %locale% } |
Où %locale%
est un paramètre défini dans app/config/parameters.yml
, et que vous pouvez mettre à « fr » si ce n'est pas déjà fait. Ainsi, tous les messages utilisés par FOSUserBundle
seront traduits en français !
Afficher une barre utilisateur
Il est intéressant d'afficher dans le layout si le visiteur est connecté ou non, et d'afficher des liens vers les pages de connexion ou de déconnexion. Cela se fait facilement, je vous invite à insérer ceci dans votre layout, où vous voulez :
1 2 3 4 5 6 7 | {# app/Resources/views/layout.html.twig #} {% if is_granted("IS_AUTHENTICATED_REMEMBERED") %} Connecté en tant que {{ app.user.username }} - <a href="{{ path('fos_user_security_logout') }}">Déconnexion</a> {% else %} <a href="{{ path('fos_user_security_login') }}">Connexion</a> {% endif %} |
Adaptez et mettez ce code dans votre layout, effet garanti.
Le rôle IS_AUTHENTICATED_REMEMBERED
est donné à un utilisateur qui s'est authentifié soit automatiquement grâce au cookie remember_me
, soit en utilisant le formulaire de connexion. Le rôle IS_AUTHENTICATED_FULLY
est donné à un utilisateur qui s'est obligatoirement authentifié manuellement, en rentrant son mot de passe dans le formulaire de connexion. C'est utile pour protéger les opérations sensibles comme le changement de mot de passe ou d'adresse e-mail.
Manipuler les utilisateurs avec FOSUserBundle
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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php // Dans un contrôleur : // Pour récupérer le service UserManager du bundle $userManager = $this->get('fos_user.user_manager'); // Pour charger un utilisateur $user = $userManager->findUserBy(array('username' => 'winzou')); // Pour modifier un utilisateur $user->setEmail('cetemail@nexiste.pas'); $userManager->updateUser($user); // Pas besoin de faire un flush avec l'EntityManager, cette méthode le fait toute seule ! // Pour supprimer un utilisateur $userManager->deleteUser($user); // Pour récupérer la liste de tous les utilisateurs $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('SdzUserBundle: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'un article 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.
- La sécurité se compose de deux couches :
- L'authentification, qui définit qui est le visiteur ;
- L'autorisation, qui définit si le visiteur a accès à la ressource demandée.
- Le fichier
security.yml
permet de configurer finement chaque acteur de la sécurité : - La configuration de l'authentification passe surtout par le paramétrage d'un ou plusieurs pare-feu ;- La configuration de l'autorisation se fait au cas par cas suivant les ressources : on peut sécuriser une méthode de contrôleur, un affichage ou une URL.
- Les rôles associés aux utilisateurs définissent les droits dont ils disposent ;
- On peut configurer la sécurité pour utiliser
FOSUserBundle
, un bundle qui offre un espace membres presque clé en main.