Licence CC BY-NC-SA

Sécurité et gestion des utilisateurs

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

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.

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 :

  1. Le visiteur n'est pas identifié, il est anonyme, et tente d'accéder à la page /foo.
  2. 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.
  3. 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.
  4. 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.

Schéma du processus de sécurité

Voici le processus pas à pas :

  1. Le visiteur n'est pas identifié, il est toujours anonyme, et tente d'accéder à la page /admin/foo.
  2. 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.
  3. Le contrôle d'accès regarde si la page /admin/foo requiert des droits d'accès : oui, il faut le rôle ROLE_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.
  4. 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.

Schéma du processus de sécurité

  1. 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.
  2. Le contrôle d'accès regarde si la page /admin/foo requiert des droits d'accès : oui, il faut le rôle ROLE_ADMIN, que Ryan n'a pas. Il interdit donc l'accès à la page /admin/foo à Ryan.
  3. 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.

Schéma du processus de sécurité

  1. 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.
  2. Le contrôle d'accès regarde si la page /admin/foo requiert des droits d'accès : oui, il faut le rôle ROLE_ADMIN, qu'admin a bien. Il laisse donc passer l'utilisateur.
  3. 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 :

  1. Un utilisateur veut accéder à une ressource protégée ;
  2. Le firewall redirige l'utilisateur au formulaire de connexion ;
  3. L'utilisateur soumet ses informations d'identification (par exemple login et mot de passe) ;
  4. Le firewall authentifie l'utilisateur ;
  5. L'utilisateur authentifié renvoie la requête initiale ;
  6. 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-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, 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.

Je suis authentifié en tant qu'anonyme

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 valeur in_memory correspond au fournisseur défini dans la section providers 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 route login, 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.

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.

Je suis bien authentifié

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.

L'accès est interdit

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 ou false 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% }

%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.