Symfony2 : créer un admin sécurisé

Non je ne suis pas mort ! Une grosse année qui arrive à son terme et qui m’aura un peu éloigné du blog ces derniers mois, mais me voilà de retour aux affaires avec la sortie imminente de Symfony2. J’ai donc décidé de monter un petit site pour tester en conditions réelles ce nouveau framework. Et tant qu’à débroussailler le terrain, autant faire partager mes difficultés.
L’idée dans le tuto d’aujourd’hui est de faire un petit point sur la partie « security » très bien documentée sur le site officiel mais qui change assez des habitudes de symfony 1 et donc semble un peu déroutante au début.
Ce billet est issu de ma propre et récente expérience sur le sujet et sur la branche master de fabien. Les choses peuvent changer ou être inexactes. Dans ce dernier cas, n’hésitez pas à me reprendre via les commentaires ;)
Contexte
Pour l’illustration, je vais simplement partir de la sandbox officielle d’origine configurée sur l’url suivante en local chez moi :
http://sandbox.localet où je vais vous montrer comment y intégrer votre admin avec un formulaire d’identification sur l’url suivante :
http://sandbox.local/admin/
L’utilisateur sera géré directement via la config, pas de bdd, on veut quelque chose de très simple et rapide à mettre en place.
Première chose à comprendre dans une application Symfony2, il n’y a plus qu’une application… Oui hein, ça parait fou dis comme ça :D Toute la séparation se situe au niveau des bundles, chacun d’eux pouvant embarquer des modèles, controllers, vues différentes, tout en pouvant communiquer avec les autres bundles. Fini les galères pour faire des liens entre un backend et un frontend !
Pour notre admin, nous allons donc générer un nouveau bundle dans notre application via la commande :
php app/console init:bundle Application/AdminBundle
On se retrouve donc avec un controller basique dans ce bundle qui sera la base de notre admin. Première chose à faire, l’activer dans le fichier app/AppKernel.php via la méthode registerBundles.
Mise en place url admin
On va maintenant tenter d’y accéder via l’url /admin/
Pour ça, c’est très simple ! On va modifier notre fichier routing.yml (ou xml ou php hein, mais moi j’aime bien le yml ! ) comme ceci :
#path:app/config/routing.yml
homepage:
pattern: /
defaults: { _controller: FrameworkBundle:Default:index }
hello:
resource: HelloBundle/Resources/config/routing.yml
admin:
resource: AdminBundle/Resources/config/routing.yml
prefix: /adminet du coup créer le fichier de routing de notre nouveau AdminBundle. Pour ça, on va faire quelque chose de très simple aussi:
#src/Application/AdminBundle/Resources/config/routing.yml
admin_homepage:
pattern: /
defaults: { _controller: AdminBundle:Default:index }Et par magie, maintenant, si vous accédez à l’url :
http://sandbox.local/admin/
Vous obtenez le mot « Hello » qui n’est d’autre que la vue par défaut qui a été générée par la commande init:bundle
Pour pas se perdre, on va modifier la vue, pour y indiquer le mot admin, ca sera plus clair.
Sécuriser l’url d’admin
2e étape, comment sécuriser tout ça pour ne pas laisser tout le monde accéder à votre admin. Pour ça il y a en fait plusieurs solutions.
- Sécuriser toutes les urls de votre application, en autorisant les connexions anonymes partout, sauf dans votre admin
- Sécuriser toutes les urls de votre admin seulement
La 1ere a l’avantage de faire écrire très peu de ligne de config, mais honnêtement, je ne sais pas s’il y en a une de meilleure que l’autre, donc je présente la première dans ce tuto, mais je vous met le code de la deuxième à la fin. Elles font sensiblement la même chose.
On va donc se lancer dans la config du component security. Pour ça, on se retrouve dans notre config.yml pour y ajouter un provider, qui sera en fait le compte utilisateur de notre admin.
#app/config/config.yml
security.config:
providers:
admin:
users:
monadmin: { password: monpass, roles: ROLE_ADMIN }Nous voici avec un compte « monadmin » identifié grâce au mot de passe « monpass » qui sera crédité de l’autorisation « ROLE_ADMIN » (pour pourriez mettre TOTO à la place de ADMIN, ca changerait rien, par contrel mot ROLE est apparemment obligatoire)
Pour l’instant, ca ne change rien. Définissons maintenant une règle firewall ! Le mot utilisé pour définir ce nouveau service fait sourire j’avoue, en fait il s’agit de définir des règles de sécurité pour des motifs d’urls.
On reste dans notre config.yml qu’on enrichit comme ceci:
#app/config/config.yml
security.config:
providers:
main:
users:
monadmin: { password: monpass, roles: ROLE_ADMIN }
firewalls:
admin:
pattern: .*
http_basic: trueEt là, vous obtenez normalement une demande d’authentification HTTP sur tout votre site. Ok! Maintenant, on va autoriser des connexions anonymes, c’est à dire qu’on ne va pas forcer les gens à se connecter:
#app/config/config.yml
security.config:
providers:
main:
users:
monadmin: { password: monpass, roles: ROLE_ADMIN }
firewalls:
admin:
pattern: .*
http_basic: true
anonymous: trueMaintenant, voyons voir pour mettre en place un formulaire plus conventionnel (et il me semble plus secure si bien fait, mais à confirmer).
#app/config/config.yml
security.config:
providers:
main:
users:
monadmin: { password: monpass, roles: ROLE_ADMIN }
firewalls:
admin:
pattern: .*
form_login: true
anonymous: trueIci on a changé le mode d’authentification pour celui d’un formulaire classique, grâce à l’option form-login
Maintenant, on va verrouiller l’accès à notre admin. Pour ce faire, on va demander une autorisation pour notre url /admin:
#app/config/config.yml
security.config:
providers:
main:
users:
monadmin: { password: monpass, roles: ROLE_ADMIN }
firewalls:
admin:
pattern: .*
form_login: true
anonymous: true
access_control:
- { path: /admin/.*, role: ROLE_ADMIN }Et maintenant, si vous essayez d’accéder à l’url :
http://sandbox.local/admin/
Vous êtes redirigés vers l’url:
http://sandbox.local/login/
qui se termine en 404! Jusqu’ici c’est normal.
Symfony2 par défaut, utilise 3 urls pour la gestion de l’authentification :
- login
- login_check
- logout
Nous allons donc les rajouter dans notre routing.yml tout simplement. Attention de bien les mettre au début, histoire d’éviter tout conflit avec une autre de vos règles.
_security_login:
pattern: /login
defaults: { _controller: AdminBundle:Default:login }
_security_check:
pattern: /login_check
_security_logout:
pattern: /logoutLe login a besoin de nous pour constituer le formulaire d’identification, les 2 autres doivent seulement exister ! Les patterns des urls peuvent varier, mais il faut alors donner les nouveaux paths dans le config.yml, nous verrons ça plus tard.
Maintenant, si nous accédons à notre admin nous avons bien sûr l’exception suivante:
Method "Application\AdminBundle\Controller\DefaultController::loginAction" does not exist.
On la crée donc:
<?php // src/Application/AdminBundle/Controller/DefaultController.php namespace Application\AdminBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\Security\SecurityContext; // à rajouter class DefaultController extends Controller { public function indexAction() { return $this->render('AdminBundle:Default:index.php'); } public function loginAction() { // get the error if any (works with forward and redirect -- see below) if ($this->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) { $error = $this->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR); } else { $error = $this->get('request')->getSession()->get(SecurityContext::AUTHENTICATION_ERROR); } return $this->render('AdminBundle:Default:login.twig', array( // last username entered by the user 'last_username' => $this->get('request')->getSession()->get(SecurityContext::LAST_USERNAME), 'error' => $error, )); } }
Donc là, j’ai simplement récupéré le code fourni dans la doc officielle, pas de mystère non plus. Et voici la vue correspondante :
<form action="{% path "_security_check" %}" method="post"> {% if error %} <div class="notification error png_bg"> <div> {{ error }} </div> </div> {% endif %} <p> <label>Username</label> <input class="text-input" type="text" name="_username" value="{{ last_username }}" /> </p> <p> <label>Password</label> <input class="text-input" type="password" name="_password" /> </p> <p id="remember-password"> <input type="checkbox" />Remember me </p> <p> <input class="button" type="submit" name="login" value="Sign In" /> </p> </form>
Là aussi, très basique mais ça vous donne l’occasion d’essayer twig ;) Ce qui compte pour le coup, c’est surtout les attributs name des input (_username et _password), et de rediriger vers /login_check
A noter que l’utilisation du tag path dans la dernière version de twig n’est plus d’actualité. C’est désormais une fonction, donc à utiliser comme ceci {{ path(‘_security_check’) }}
Alors oui, il faudrait un layout et tout, mais bon, on va aller droit à l’essentiel.
On retrouve bien notre formulaire de login maintenant, si on essaye d’accéder à notre admin ! Encore mieux, si on essaye de s’authentifier, avec le couple login/password défini au début, on obtient notre page « admin! » Et il suffit d’ajouter une dernière ligne à notre config, pour ajouter le listener manquant sur le /logout :
#app/config/config.yml
security.config:
providers:
main:
users:
monadmin: { password: monpass, roles: ROLE_ADMIN }
firewalls:
admin:
pattern: .*
form_login: true
anonymous: true
logout: true
access_control:
- { path: /admin/.*, role: ROLE_ADMIN }Et maintenant, vous pouvez vous déloguer sur l’url :
http://sandbox.local/logout
Histoire d’illustrer un peu plus, voici une autre config qui fonctionne aussi et qui permet de ne sécuriser que l’admin:
security.config:
providers:
main:
users:
monadmin: { password: monpass, roles: ROLE_TOTO }
firewalls:
admin: { pattern: /admin/.*, form_login: true }
login: { pattern: /login, anonymous: true, form_login: true }
login_check: { pattern: /login_check, anonymous: true, form_login: true }
logout: { pattern: /logout, form_login: true, logout: true }
access_control:
- { path: /admin/.*, role: ROLE_TOTO }Il y a bien sûr, beaucoup de tuning possible, pour encoder le password par exemple, mais maintenant qu’on a la base, je vous laisse lire vraiment la doc entière cette fois-ci ;)
Tags: admin, config, Symfony, symfony2, Tutorials
10 Réponses
Laisser un message

Hello,
Cool l’article! Je dois dire que je fais sensiblement la même chose, et ca marche bien pour l’instant.
Par contre j’ai des doutes quant à la souplesse du firewall.
Est-ce que toute les notions de securité peuvent etre gerées uniquement en fonction d’un pattern d’url? ( j’aimerai bien des avis sur la question :) ).
Par contre, a mon humble avis, il y a un probleme d’indentation au niveau de la definition des « access_control » ( a part dans le dernier exemple )
Il devrait etre placé au meme niveau que « firewalls » et « providers ».
Exact pour l’indentation, j’ai corrigé!
Pour la souplesse, oui tu peux gérer en fonction d’un seul pattern, j’ai vérifié et le premier exemple le montre.
Pour moi l’idéal c’est de le gérer comme ça mais à partir du pattern de ton admin, et du coup de redéfinir les path du login, login_check, logout
Pour mon projet, je l’ai géré comme ça du coup, ca fait plus de ligne, mais j’ai une seule définition de firewall également.
Mais je suis également curieux de connaitre l’avis d’autres qui jongleraient avec ça depuis plus longtemps!
Bonjour,
l’exemple est TRES intéressant pour un habitué des frameworks. Pour les autres, n’oubliez pas de vider le cache de SYMFONY (fichiers à supprimer : appdev* se trouvant dans SANDBOX/APP/CHACHE/DEV) avant de tester votre nouveau contrôleur. Ces fichiers sont auto-générés par l’injection des dépendances et je n’ai pas encore étudié si SYMFONY les supprime tout seul comme un grand ou si l’on doit le faire manuellement.
@kalamityJane oui, il les supprime tout seul comme un grand si tu es dans l’environnement de dev normalement.
Non, pas dans mon installation semble-t-il puisque je dois le faire à la main. Qu’aurais-je bien pu oublier ?
Env : Windows 7, easyPHP, PHP 5.3
Le cache est vérifié avec la fonction filemtime. je sais pas comment ca fonctionne sous windows. cva vient peut etre de la ?
Y aurait-il un problème de conversion de date ?
Pour compléter ce que j’ai annoncé concernant l’épuration manuelle du cache, voici un lien dans lequel je retrouve cette phrase intéressante
« …Si on vide le cache et que l’on rappelle une url sur le projet,… »
voici le lien, très intéressant pour un débutant en framework (comme moi)…
http://www.elao.org/symfony-2/symfony-2-linjection-de-dependances.html
Un ./app/console cache:warmup est equivalent à rappeler l’url sur le projet.
Bonjour,
Bon résumé, et bonne méthode progressive. Je souhaitais simplement répondre à une question que vous vous posez ici : « quelle différence entre les deux méthodes ? ». Il y en a une et elle est de taille :
Dans la première, tout utilisateur, connecté ou non, sera considéré comme authentifié. Un anonyme est un utilisateur authentifié. Il n’a cependant pas de rôle spécifique. Dans la deuxième méthode, en dehors des URLs gérées par le firewall, l’utilisateur n’existe pas quand il navigue.
Il est intéressant, voire important que tout utilisateur soit authentifié, même s’il n’est pas connecté (pas de compte), car cela active différents mécanismes dont la documentation parle de temps en temps. Certaines fonctionnalités ne sont tout simplement pas actives sur une URL non firewallée ».
Alors évidemment, au niveau connexion d’un admin vos deux méthodes sont identiques, et c’était votre point ici, mais il me semble utile de préciser ce que ça change sur le fond.
Attention aussi : les firewall, quand il y en a plusieurs, ne partagent pas les informations de connexion et session, ce qui peut vite compliquer la tâche. Autant mettre un firewall à la racine (pattern: ^/) puis tout gérer avec les contrôles d’accès.
Voilà :)