• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 5/13/19

Sécurité et gestion des utilisateurs

Log in or subscribe for free to enjoy all this course has to offer!

Dans ce chapitre, nous allons apprendre la sécurité avec Symfony. 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 Symfony. Nécessaire, elle nous permettra d'aborder la deuxième étape : l'installation du bundleFOSUserBundle, qui viendra compléter notre espace membres.

Bonne lecture !

Authentification et autorisation

La sécurité sous Symfony est très poussée, vous pouvez la contrôler très finement, mais surtout très facilement. Pour atteindre ce but, Symfony 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 Symfony s'appelle un firewall, ou un pare-feu en français.

Ainsi vous pourrez sécuriser des parties de votre site Internet juste en forçant le visiteur à être un membre authentifié. Si le visiteur l'est, le firewall va le laisser passer, sinon il le redirigera sur la page d'identification. Cela se fera donc dans les paramètres du firewall, nous les verrons plus en détail par la suite.

L'autorisation

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 Symfony 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, c'est 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/fooqui 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é
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. 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/foorequiert 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/fooqui 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/foorequiert le rôleROLE_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é
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/foorequiert 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.

  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/fooqui 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é
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/foorequiert 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.

  3. Ryan n'a pas accès à la page/admin/foonon 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/fooqui requiert des droits que j'ai.

Ici, nous sommes maintenant identifiés en tant qu'administrateur, on a donc le rôleROLE_ADMIN! Du coup, nous pouvons accéder à la page/admin/foo, comme le montre la figure suivante.

Schéma du processus de sécurité
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/foorequiert des droits d'accès : oui, il faut le rôleROLE_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 Symfony (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 fichiersecurity.yml, situé dans le répertoireapp/configde votre application. Il est un peu vide pour le moment, je vous propose déjà de rajouter quelques sections que l'on décrit juste après. Votre fichier doit ressembler à ceci :

# app/config/security.yml

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 toute cette configuration 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.

Sectionencoders
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 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éplaintextn'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 !

Sectionrole_hierarchy
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 pour accéder aux ressources on demande que l'utilisateur ait 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ôleROLE_USERest compris dans le rôleROLE_ADMIN. Cela signifie que si votre page requiert le rôleROLE_USER, et qu'un utilisateur disposant du rôleROLE_ADMINtente d'y accéder, il sera autorisé, car en disposant du rôle d'administrateur, il dispose également du rôleROLE_USER.

Les noms des rôles n'ont pas d'importance, si ce n'est qu'ils doivent commencer par «ROLE_».

Sectionproviders
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 et 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 pour faire du développement, pour tester la couche sécurité sans avoir besoin d'une quelconque base de données derrière. Il faudra bien sûr le supprimer par la suite.

Je vous rassure, il existe d'autres types de fournisseurs que celui-ci. On utilisera notamment par la suite un fournisseur permettant de récupérer les utilisateurs dans la base de données, il est déjà bien plus intéressant.

Sectionfirewalls
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-feudevest 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.

Sectionaccess_control
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 appelleronsmain, comme ceci :

# 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 :

  • mainest 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: trueaccepte les utilisateurs anonymes. Nous protégerons nos ressources grâce aux rôles.

Si vous actualisez n'importe quelle page de votre site, vous pouvez maintenant voir dans la barre d'outils en bas que vous êtes authentifié en tant qu'anonyme, comme sur la figure suivante.

Je suis authentifié en tant qu'anonyme
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 : le firewall les a bien reconnu comme étant des anonymes. Mais ils restent des anonymes, et si nous mettions la valeur du paramètreanonymousàfalse dans la configuration, on se ferait bien refuser l'accè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'optionform_login, entre autres, qu'il faut rajouter à notre pare-feu :

# app/config/security.yml

security:
  provider:     in_memory
  firewalls:
    # ...
    main:
      pattern:      ^/
      anonymous:    true
      form_login:
        login_path: login
        check_path: login_check
      logout:
        path:       logout
        target:     login

Expliquons les quelques nouvelles lignes :

  • provider: in_memoryest 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_memorycorrespond au nom du fournisseur défini dans la sectionprovidersqu'on a vu plus haut.

  • form_loginest 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: logincorrespond à 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éfinirons juste après.

    • check_path: login_checkcorrespond à 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.

  • logoutrend possible la déconnexion. En effet, par défaut il est impossible de se déconnecter une fois authentifié. Ses options sont les suivantes :

    • pathest le nom de la route à laquelle le visiteur doit aller pour être déconnecté. On va la définir plus loin.

    • targetest le nom de la route 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 routelogin). On va créer ce formulaire juste après, il devra envoyer les valeurs (nom d'utilisateur et mot de passe) vers la route (ici,login_check) qui va prendre en charge la gestion du formulaire.

Nous nous occupons de l'affichage du formulaire, mais c'est le système de sécurité de Symfony qui va s'occuper de la gestion de ce formulaire. Concrètement, nous allons définir un contrôleur à exécuter pour la routelogin, mais pas pour la routelogin_check! Symfony va attraper la requête de notre visiteur sur la routelogin_check, et gérer lui-même l'authentification. En cas de succès, le visiteur sera authentifié. En cas d'échec, Symfony le renverra vers notre formulaire de connexion pour qu'il réessaie.

Voici alors les trois routes à définir dans le fichierrouting.yml:

# app/config/routing.yml

# ...

login:
    path: /login
    defaults:
        _controller: OCUserBundle:Security:login

login_check:
    path: /login_check

logout:
    path: /logout

Comme vous pouvez le voir, on ne définit pas de contrôleur pour les routeslogin_checketlogout. Symfony 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 bundleOCUserBundle

Cela ne vous a pas échappé, j'ai défini le contrôleur à exécuter sur la routelogincomme étant dans le bundleOCUserBundle. 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 :

php bin/console generate:bundle

Avant de continuer, je vous propose un petit nettoyage dans ce nouveauOCUserBundle, car le générateur a tendance à trop en faire. Vous pouvez donc supprimer allègrement :

  • Le contrôleurController/DefaultController.php;

  • Son répertoire de testsTests/Controller;

  • Son répertoire de vuesResources/views/Default;

  • Le fichier de routesResources/config/routing.yml ;

  • La ligne d'import (oc_user) du fichier de routes dans le fichier app/config/routing.yml.

Créer le formulaire de connexion

Il s'agit maintenant de créer le formulaire de connexion, disponible sur la routelogin, soit l'URL/login. Commençons par le contrôleur :

<?php
// src/OC/UserBundle/Controller/SecurityController.php;

namespace OC\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class SecurityController extends Controller
{
  public function loginAction(Request $request)
  {
    // Si le visiteur est déjà identifié, on le redirige vers l'accueil
    if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
      return $this->redirectToRoute('oc_platform_accueil');
    }

    // Le service authentication_utils permet de récupérer le nom d'utilisateur
    // et l'erreur dans le cas où le formulaire a déjà été soumis mais était invalide
    // (mauvais mot de passe par exemple)
    $authenticationUtils = $this->get('security.authentication_utils');

    return $this->render('OCUserBundle:Security:login.html.twig', array(
      'last_username' => $authenticationUtils->getLastUsername(),
      'error'         => $authenticationUtils->getLastAuthenticationError(),
    ));
  }
}

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 Symfony 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 :

{# src/OC/UserBundle/Resources/views/Security/login.html.twig #}

{% extends "OCCoreBundle::layout.html.twig" %}

{% block body %}

  {# S'il y a une erreur, on l'affiche dans un joli cadre #}
  {% if error %}
    <div class="alert alert-danger">{{ 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
Le formulaire de connexion

Lorsque j'entre de faux identifiants, l'erreur générée est celle visible à la figure suivante.

Mauvais identifiants
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é
Je suis bien authentifié

Mais quels sont les bons identifiants ?

Il faut lire attentivement le fichier de configuration qu'on a parcouru précédemment. Rappelez-vous, on a défini le fournisseur d'utilisateur 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 sa 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 routeslogin,login_checketlogout. 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_checkderrière le pare-feu

Vous devez vous assurer que l'URL ducheck_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, lepattern: ^/du pare-feumainprend 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/loginne requière aucun rôle, on fera attention à cela lorsqu'on va définir les autorisations.

De plus, si vous souhaitez interdire les anonymes sur le pare-feumain, le problème se pose également, car un nouvel arrivant sera forcément anonyme et ne pourra pas accéder au formulaire de connexion. L'idée dans ce cas est de sortir le formulaire de connexion (la page/login) du pare-feumain. En effet, c'est lecheck_pathqui doit obligatoirement appartenir au pare-feu, pas le formulaire en lui-même. Si vous souhaitez interdire les anonymes sur votre site (et uniquement dans ce cas), vous pouvez donc vous en sortir avec la configuration suivante :

# app/config/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-feumain, 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 servicesecurity.token_storage.

Ce service dispose d'une méthodegetToken(), 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 vautnullsi 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 :

<?php

// On récupère le service
$security = $container->get('security.token_storage');

// 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 :

  • nullsi la requête n'est pas derrière un pare-feu, ou si l'utilisateur courant est anonyme ;

  • Une instance deUserle reste du temps (utilisateur authentifié derrière un pare-feu et non-anonyme).

Du coup, voici le code simplifié depuis un contrôleur :

<?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 }}:

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

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 fichiersecurity.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ôleROLE_ADMIN(par exemple).

Tout d'abord, essayons d'imaginer les rôles dont on aura besoin dans notre application de plateforme d'annonce. Je pense à :

  • ROLE_AUTEUR: pour ceux qui ont le droit d'écrire des annonces ;

  • ROLE_MODERATEUR: pour ceux qui peuvent modérer les annonces ;

  • 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 aROLE_AUTEURou s'il aROLE_ADMIN, alors il peut écrire une annonce ». Grâce à la définition de la hiérarchie, on peut faire simplement « si l'utilisateur aROLE_AUTEUR». Car un utilisateur qui dispose deROLE_ADMINdispose également deROLE_AUTEUR, c'est une inclusion.

Ce sont ces relations, et uniquement ces relations, que nous allons inscrire dans le fichiersecurity.yml. Voici donc comment décrire dans la configuration la hiérarchie qu'on vient de définir :

# app/config/security.yml

security:
    role_hierarchy:
        # Un admin hérite des droits d'auteur et de modérateur
        ROLE_ADMIN:       [ROLE_AUTEUR, ROLE_MODERATEUR]
        # On garde ce rôle superadmin, il nous resservira par la suite
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

Remarquez que je n'ai pas utilisé le rôleROLE_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ôleROLE_AUTEUR, alors il peut écrire une annonce. Les auteurs et les admins peuvent donc le faire.

  • Si l'utilisateur a le rôleROLE_ADMIN, alors il peut supprimer une annonce. Seuls les admins peuvent donc le faire.

Tous ces tests nous permettront de limiter l'accès à nos différentes pages.

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 servicesecurity.authorization_checker, Twig, et les contrôles d'accès. Ce sont quatre façons de faire exactement la même chose.

Utiliser directement le servicesecurity.authorization_checker

Ce n'est pas le moyen le plus court, mais c'est celui par lequel passent les trois autres méthodes. Il faut donc que je vous en parle en premier !

Depuis votre contrôleur ou n'importe quel autre service, il vous faut accéder au service  security.authorization_checker et appeler la méthodeisGranted, tout simplement. Par exemple dans notre contrôleur :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

class AdvertController extends Controller
{
  public function addAction(Request $request)
  {
    // On vérifie que l'utilisateur dispose bien du rôle ROLE_AUTEUR
    if (!$this->get('security.authorization_checker')->isGranted('ROLE_AUTEUR')) {
      // Sinon on déclenche une exception « Accès interdit »
      throw new AccessDeniedException('Accès limité aux auteurs.');
    }

    // Ici l'utilisateur a les droits suffisant,
    // on peut ajouter une annonce
  }
}

C'est tout ! Vous pouvez aller sur /platform, mais impossible d'atteindre la page d'ajout d'une annonce sur /platform/add, car vous ne disposez pas (encore !) du rôleROLE_AUTEUR, comme le montre la figure suivante.

L'accès est interdit
L'accès est interdit
Utiliser les annotations dans un contrôleur

Pour faire exactement ce qu'on vient de faire avec le servicesecurity.authorization_checker, il existe un moyen bien plus rapide et joli : les annotations !

L'annotation@Security que nous allons utiliser ici provient du bundleSensioFrameworkExtraBundle, c'est un bundle qui apporte quelques petits plus au framework. Pas besoin d'explication, son utilisation basique est assez simple ; regardez le code :

<?php
// src/OC/PlatformBundle/Controller/AdvertController.php

namespace OC\PlatformBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
// N'oubliez pas ce use pour l'annotation
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

class AdvertController extends Controller
{
  /**
   * @Security("has_role('ROLE_AUTEUR')")
   */
  public function addAction(Request $request)
  {
    // 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
  }
}

Et voilà ! Grâce à l'annotation@Security, on a sécurisé notre méthode en une seule ligne, vraiment pratique.

La valeur de l'option par défaut de l'annotation est en fait une expression, dans laquelle vous pouvez utiliser plusieurs variables et fonctions (dont has_role qu'on a utilisé ici). Si vous voulez vérifier que l'utilisateur a deux rôles, vous pouvez faire comme ceci :

<?php
/**
 * @Security("has_role('ROLE_AUTEUR') and has_role('ROLE_AUTRE')")
 * /

Le détail des variables et fonctions disponibles est dans la documentation.

Pour vérifier simplement que l'utilisateur est authentifié, et donc qu'il n'est pas anonyme, vous pouvez utiliser le rôle spécialIS_AUTHENTICATED_REMEMBERED.

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 une annonce ne doit être visible que pour les membres qui disposent du rôleROLE_AUTEUR(car c'est la contrainte que nous avons mise sur la méthodeaddAction()).

Pour cela, Twig dispose d'une fonctionis_granted()qui est en réalité un raccourci pour exécuter la méthodeisGranted()du servicesecurity.authorization_checker. La voici en application :

{# On n'affiche le lien « Ajouter une annonce » qu'aux auteurs
  (et admins, qui héritent du rôle auteur) #}
{% if is_granted('ROLE_AUTEUR') %}
  <li><a href="{{ path('oc_platform_add') }}">Ajouter une annonce</a></li>
{% 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 (toutes les pages dont l'URL commence par/admin) en une seule ligne :

# app/config/security.yml

security:
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

Ainsi, toutes les URL qui correspondent aupath(ici, toutes celles qui commencent par/admin) requièrent le rôleROLE_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 :

# app/config/security.yml

security:
    access_control:
        - { path: ^/admin, ip: 127.0.0.1, requires_channel: https }
Pour conclure sur les méthodes de sécurisation

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

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 Symfony, 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 classeUserqui 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éUserau sein du bundleOCUserBundle, grâce au générateur de Doctrine (php bin/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 :

<?php

namespace OC\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="oc_user")
 * @ORM\Entity(repositoryClass="OC\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 = array();

  // Les getters et setters

  public function eraseCredentials()
  {
  }
}

Et pour que Symfony l'accepte comme classe utilisateur de la couche sécurité, il faut qu'on implémente l'interfaceUserInterface:

<?php
// src/OC/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 !

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 :

<?php
// src/OC/UserBundle/DataFixtures/ORM/LoadUser.php

namespace OC\UserBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use OC\UserBundle\Entity\User;

class LoadUser implements FixtureInterface
{
  public function load(ObjectManager $manager)
  {
    // Les noms d'utilisateurs à créer
    $listNames = array('Alexandre', 'Marine', 'Anna');

    foreach ($listNames as $name) {
      // On crée l'utilisateur
      $user = new User;

      // Le nom d'utilisateur et le mot de passe sont identiques pour l'instant
      $user->setUsername($name);
      $user->setPassword($name);

      // On ne se sert pas du sel pour l'instant
      $user->setSalt('');
      // On définit uniquement le role ROLE_USER qui est le role de base
      $user->setRoles(array('ROLE_USER'));

      // On le persiste
      $manager->persist($user);
    }

    // On déclenche l'enregistrement
    $manager->flush();
  }
}

Exécutez cette fois la commande :

php bin/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 classeUserutilisée. Or maintenant nous allons nous servir d'une autre classe, il s'agit deOC\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'encodeurplaintext, 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 :

# app/config/security.yml

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        OC\UserBundle\Entity\User: plaintext

Définissons le fournisseur d'utilisateurs

On en a parlé plus haut, il faut définir un fournisseur (provider) pour que le pare-feu puisse identifier et récupérer les utilisateurs.

Qu'est-ce qu'un fournisseur d'utilisateurs, concrètement ?

Un fournisseur d'utilisateurs est une classe qui implémente l'interfaceUserProviderInterface, 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 finalement pas grand-chose, à part charger ou rafraîchir les utilisateurs.

Symfony dispose déjà de trois types de fournisseurs, qui implémentent tous l'interface précédente évidemment, les voici :

  • memoryutilise les utilisateurs définis dans la configuration, c'est celui qu'on a utilisé jusqu'à maintenant ;

  • entityutilise de façon simple une entité pour fournir les utilisateurs, c'est celui qu'on va utiliser ;

  • idpermet d'utiliser un service quelconque en tant que fournisseur, en précisant le nom du service.

Créer notre fournisseurentity

Il est temps de créer le fournisseurentitypour notre entité User. Celui-ci existe déjà dans Symfony, nous n'avons donc pas de code à faire, juste un peu de configuration. On va l'appeler « main », un nom arbitraire. Voici comment le déclarer :

# app/config/security.yml

security:
  providers:
    # … vous pouvez supprimer le fournisseur « in_memory »
    # Et voici notre nouveau fournisseur :
    main:
      entity:
        class:    OC\UserBundle\Entity\User
        property: username

Il y a deux paramètres à préciser pour le fournisseur :

  • La classe de l'entité à utiliser évidemment, il s'agit pour le fournisseur de savoir quel repository Doctrine utiliser pour ensuite charger nos entités. Vous pouvez également  utiliser le nom logique de l'entité, iciOCUserBundle:User ;

  • L'attribut de la classe qui sert d'identifiant, on utiliseusername, 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 fournisseurin_memory. Pour cela, modifions simplement la valeur du paramètreprovider, comme ceci :

# app/config/security.yml

security:
  firewalls:
    main:
      pattern:   ^/
      anonymous: true
      provider:  main # On change cette valeur
      # … reste de la configuration du pare-feu

Manipuler vos utilisateurs

La couche sécurité est maintenant pleinement opérationnelle et utilise des utilisateurs stockés en base de données. Testez-le dès maintenant en vous identifiant avec le nom d'utilisateur et mot de passe définis dans le fichier de fixtures (vous aurez peut-être besoin de faire uncache:clear  d'abord). 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éUserque nous avons créée est une entité tout à fait comme les autres. À ce stade du cours vous savez ajouter, modifier et supprimer des annonces, alors il en va de même pour cette nouvelle entité qui représente vos utilisateurs.

Bref, faites-vous confiance, vous avez toutes les clés en main pour manipuler entièrement vos utilisateurs.

Cependant, toutes les pages d'un espace membres sont assez classiques : inscription, mot de passe perdu, modification du profil, etc. Tout cela est du déjà-vu. Et si c'est déjà vu, il existe déjà certainement un bundle pour cela. Et je vous le confirme, il existe même un excellent bundle, il s'agit deFOSUserBundleet 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'appelleFOSUserBundle, il est très utilisé par la communauté Symfony 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 leUserqu'on vient de développer, cela fonctionne tout aussi bien !

Installation deFOSUserBundle

Télécharger le bundle

Le bundleFOSUserBundleest hébergé sur GitHub, comme beaucoup de bundles et projets Symfony. 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 fichiercomposer.json:

// composer.json

{
  // …

  "require": {
    // …
    "friendsofsymfony/user-bundle": "dev-master"
  }

  // …
}

Ensuite, il faut dire à Composer d'installer cette nouvelle dépendance :

php composer.phar update friendsofsymfony/user-bundle
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 Symfony. Pour cela, ouvrez le fichierapp/AppKernel.phppour enregistrer le bundle :

<?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 Symfony 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ériterFOSUserBundledepuis notreOCUserBundle

FOSUserBundleest 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 fichierOCUserBundle.phpqui représente notre bundle, et modifiez-le comme suit :

<?php
// src/OC/UserBundle/OCUserBundle.php

namespace OC\UserBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class OCUserBundle extends Bundle
{
  public function getParent()
  {
    return 'FOSUserBundle';
  }
}

Et c'est tout ! On a juste rajouté cette méthodegetParent(), et Symfony va savoir gérer le reste. ;)

Lorsque qu'un bundle A (notre OCUserBundle) hérite d'un bundle B (FOSUserBundle), cela signifie entre autre que :

  • si une vue du bundle A a le même nom qu'une vue du bundle B, c'est la vue du bundle A qui sera utilisée lorsque vous faites "BundleB::myView.html.twig", alors que vous mentionnez bien "BundleB" dans le nom de la vue ;

  • si un contrôleur du bundle A a le même nom qu'un contrôleur du bundle B, c'est le contrôleur du bundle A qui sera utilisé lorsque vous faites "BundleB:myController:myAction", alors que vous mentionnez bien "BundleB" dans le nom du contrôleur.

Modifier notre entitéUser

Bien que nous ayons déjà créé une entitéUser, ce nouveau bundle en contient une plus complète, qu'on va utiliser avec plaisir plutôt que de tout recoder nous-mêmes. On va donc hériter l'entité User de FOSUserBundle depuis notre entité User de notre OCUserBundle. Notre entité ne contiendra que les attributs que l'on souhaite avoir et qui ne sont pas dans celle de FOSUserBundle. En fait, notre entité ne contient plus grand-chose au final, voici ce que cela donne :

<?php
// src/OC/UserBundle/Entity/User.php

namespace OC\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;

/**
 * @ORM\Table(name="oc_user")
 * @ORM\Entity(repositoryClass="OC\UserBundle\Repository\UserRepository")
 */
class User extends BaseUser
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  protected $id;
}

Alors c'est joli, mais pourquoi est-ce que l'on a fait cela ? En fait, le bundleFOSUserBundlene définit pas vraiment l'entitéUser, il définit une mapped superclass ! Un nom un peu barbare, juste pour dire que c'est une entité abstraite, et qu'il faut en hériter pour en faire une vraie entité. C'est donc ce que nous venons juste de faire.

Cela permet en fait de garder la main sur notre entité. On peut ainsi lui ajouter des attributs (selon vos besoins), en plus de ceux déjà définis. Pour information, les attributs qui existent déjà sont :

  • username: nom d'utilisateur avec lequel l'utilisateur va s'identifier ;

  • email: l'adresse e-mail ;

  • enabled:trueoufalsesuivant 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 Doctrine 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 Doctrine.

Configurer le bundle

Ensuite, nous devons définir certains paramètres obligatoires au fonctionnement deFOSUserBundle. Ouvrez votreconfig.ymlet ajoutez la section suivante :

# 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:    OC\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 tableUseret ajoutons quelques membres pour les tests.

Mise à jour de la tableUser

Il faut maintenant mettre à jour la table des utilisateurs, vu les modifications que l'on vient de faire. D'abord, allez la vider depuis phpMyAdmin, puis exécutez la commandephp bin/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 Symfony 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 lesecurity.ymlsous 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 (vous pouvez supprimer la ligne par défaut) :

# app/config/security.yml

security:
  encoders:
    OC\UserBundle\Entity\User: sha512
Le fournisseur

Le bundle inclut son propre fournisseur en tant que service, qui utilise notre entitéUsermais avec ses propres outils. Vous pouvez donc modifier notre fournisseurmaincomme suit :

# app/config/security.yml

security:

# …

  providers:
    main:
      id: fos_user.user_provider.username

Dans cette configuration,fos_user.user_managerest le nom du service fourni par le bundleFOSUB.

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'optionremember_medans la configuration. Voici ce que cela donne :

# 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:     login
      remember_me:
        secret:     %secret% # %secret% est un paramètre de parameter

J'ai juste ajouté le dernier paramètreremember_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 parFOSUserBundle. Exécutez la commande suivante et laissez-vous guider :

php bin/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é.

FOSUserBundleoffre 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 bundleFOSUserBundle

Configuration des routes

En plus de gérer la sécurité, le bundleFOSUserBundlegè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 votrerouting.ymlles imports suivants à la suite du nôtre :

# 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, Symfony 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 commandephp bin/console debug:router pour les routes qui concernent ce bundle :

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é/loginet autres. Du coup, je vous propose de laisser le bundle gérer cela, supprimez donc les trois routeslogin,login_checketlogoutqu'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 :

# 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:     fos_user_security_login
      remember_me:
        secret:     %secret%

Il reste quelques petits détails à gérer comme la page de login qui n'est plus 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.

Intégrer les pages du bundle dans notre layout

FOSUserBundleutilise 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/FO [...] out.html.twig

On va donc tout simplement le remplacer par une vue Twig qui va étendre notre layout à nous. 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 vuelayout.html.twigsuivante :

{# src/OC/UserBundle/Resources/views/layout.html.twig #}

{# On étend notre layout #}
{% extends "OCCoreBundle::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, messages in app.session.flashbag.all() %}
    {% for message in messages %}
      <div class="alert alert-{{ key }}">
        {{ message|trans({}, 'FOSUserBundle') }}
      </div>
    {% endfor %}
  {% 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 %}

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.

Traduire les messages

FOSUBétant un bundle international, le texte est géré par le composant de traduction de Symfony. Par défaut, celui-ci est désactivé. Pour traduire le texte, il suffit donc de l'activer (direction le fichierconfig.yml) et de décommenter une des premières lignes dansframework:

# app/config/config.yml

framework:
    translator:      { fallbacks: ["%locale%"] }

%locale%est un paramètre défini un peu plus haut dans le fichier de config. et que vous pouvez mettre à « fr » si ce n'est pas déjà fait. Ainsi, tous les messages utilisés parFOSUserBundleseront 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 :

{% 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. ;)

Manipuler les utilisateurs avecFOSUserBundle

Nous allons voir les moyens pour manipuler vos utilisateurs au quotidien.

Si les utilisateurs sont gérés parFOSUserBundle, 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 unUserManager(c'est une sorte de repository avancé). Ainsi, voici les principales manipulations que vous pouvez faire avec :

<?php
// Dans un contrôleur :

// Pour récupérer le service UserManager du bundle
$userManager = $this->get('fos_user.user_manager');

// 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('OCUserBundle:User'). Et si vous voulez en savoir plus sur ce que fait le bundle dans les coulisses, n'hésitez pas à aller voir le code des contrôleurs du bundle.

Pour conclure

Ce chapitre touche à sa fin. Vous avez maintenant tous les outils en main pour construire votre espace membres, avec un système d'authentification performant et sécurisé, et des accès limités pour vos pages suivant des droits précis.

Sachez que tout ceci n'est qu'une introduction à la sécurité sous Symfony. 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 deFOSUserBundle, qui explique comment personnaliser au maximum le bundle, ainsi que l'utilisation des groupes.

Pour information, il existe également un système d'ACL, qui vous permet de définir des droits bien plus finement que les rôles. Par exemple, pour autoriser l'édition d'une annonce si on est admin ou si on en est l'auteur. Je ne traiterai pas ce point dans ce cours, mais n'hésitez pas à vous référer à la documentation à ce sujet.

  • 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 fichiersecurity.ymlpermet 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 utiliserFOSUserBundle, un bundle qui offre un espace membres presque clé en main.

  • Le code du cours tel qu'il doit être à ce stade est disponible sur la branche iteration-16 du dépot Github.

Example of certificate of achievement
Example of certificate of achievement