Jobeet FR

Le tutoriel pour Symfony2 en français

Pleine page

Sécurisation de l'application

La sécurité est un processus en deux étapes dont le but est d'empêcher un utilisateur d'accéder à une ressource dont il ne devrait pas avoir accès. Dans la première étape du processus, l'authentification, le système de sécurité identifie l'utilisateur en l'obligeant à soumettre une sorte d'identification. Une fois que le système sait qui vous êtes, la prochaine étape, appelée l'autorisation, est de déterminer si vous devriez avoir accès à une ressource donnée (il vérifie que vous avez les privilèges pour effectuer une certaine action).

Le composant de sécurité peut être configuré via la configuration de votre application à l'aide du fichier security.yml à partir du dossier app/config. Pour sécuriser notre application ajoutez ce qui suit à votre fichier security.yml:

# app/config/security.yml
 
security:
    firewalls:
        secured_area:
            pattern:    ^/
            anonymous: ~
            form_login:
                login_path:  /login
                check_path:  /login_check
 
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
 
    providers:
        in_memory:
            users:
                admin: { password: adminpass, roles: 'ROLE_ADMIN' }
 
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

Cette configuration protégera la section /admin du site (toutes les URL qui commencent par /admin) et permettra l'accès uniquement aux utilisateurs avec ROLE_ADMIN (voir la section access_control). Dans cet exemple, l'utilisateur admin est défini dans le fichier de configuration (la section providers) et le mot de passe n'est pas chiffré (encoders).

Pour authentifier les utilisateurs, un formulaire de connexion classique sera utilisé, mais nous devons le mettre en œuvre. D'abord, créez deux routes: l'une qui affiche le formulaire de connexion (par exemple /login) et une qui va gérer la soumission du formulaire de connexion (par exemple /login_check):

# src/Ens/JobeetBundle/Resources/config/routing.yml
 
login:
    pattern:   /login
    defaults:  { _controller: EnsJobeetBundle:Default:login }
login_check:
    pattern:   /login_check
 
# ...

Nous n'aurons pas besoin de mettre en œuvre un contrôleur pour l'URL /login_check puisque le pare-feu va automatiquement intercepter et transformer n'importe quel formulaire soumis à cette URL. Il est facultatif, mais utile, pour créer une route de sorte qu'il peut être utilisé pour générer l'URL de soumission du formulaire dans le template de connexion ci-dessous.

Ensuite, nous allons créer l'action qui permettra d'afficher le formulaire de connexion:

// src/Ens/JobeetBundle/Controller/DefaultController.php
 
namespace Ens\JobeetBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;
 
class DefaultController extends Controller
{
    // ...
 
    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();
 
        // get the login error if there is one
        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('EnsJobeetBundle:Default:login.html.twig', array(
            // last username entered by the user
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        ));
    }
}

Lorsque l'utilisateur soumet le formulaire, le système de sécurité gère automatiquement la soumission du formulaire pour vous. Si l'utilisateur a soumis un nom d'utilisateur ou mot de passe invalide, cette action lit l'erreur de soumission du formulaire à partir du système de sécurité de sorte qu'il peut être affiché à l'utilisateur. Votre seule tâche est d'afficher le formulaire de connexion et les erreurs de connexion qui peuvent se produire, mais le système de sécurité lui-même prend soin de vérifier le nom d'utilisateur et mot de passe soumis et l'authentification de l'utilisateur.

Enfin, nous allons créer le template correspondant:

<!-- src/Ens/JobeetBundle/Resources/views/Default/login.html.twig -->
 
{% if error %}
    <div>{{ error.message }}</div>
{% endif %}
 
<form action="{{ path('login_check') }}" method="post">
    <label for="username">Username:</label>
    <input type="text" id="username" name="_username" value="{{ last_username }}" />
 
    <label for="password">Password:</label>
    <input type="password" id="password" name="_password" />
 
    <button type="submit">login</button>
</form>

Maintenant, si vous essayez d'accéder à l'URL http://jobeet.local/app_dev.php/admin/dashboard, le formulaire de connexion apparaîtra et vous devrez entrer le nom d'utilisateur et mot de passe défini dans security.yml (admin/adminpass) pour aller dans l'administration de Jobeet.


Les fournisseurs d'utilisateurs

Lors de l'authentification, l'utilisateur soumet un ensemble d'informations d'identification (généralement un nom d'utilisateur et mot de passe). La tâche du système d'authentification est de faire correspondre ces informations à certains groupes d'utilisateurs. Alors d'où vient cette liste d'utilisateurs?

Dans Symfony2, les utilisateurs peuvent venir de n'importe où - un fichier de configuration, une table de la BDD, un Web Service, ou toute autre chose dont vous pouvez rêver. Tout ce qui fournit un ou plusieurs utilisateurs dans le système d'authentification est connu comme un "fournisseur d'utilisateur". Symfony2 est livré en standard avec les deux fournisseurs d'utilisateurs les plus courants: celui qui charge les utilisateurs à partir d'un fichier de configuration et celui qui charge les utilisateurs d'une table de la BDD.

Ci-dessus, nous avons utilisé le premier cas: les utilisateurs spécifiés dans un fichier de configuration.

providers:
    in_memory:
        users:
            admin: { password: adminpass, roles: 'ROLE_ADMIN' }

Mais vous voudrez généralement stocker les utilisateurs dans une table de la BDD. Pour ce faire, nous allons ajouter une nouvelle table user dans notre BDD.

D'abord nous allons créer l'ORM pour cette nouvelle table:

# src/Ens/JobeetBundle/Resources/config/doctrine/User.orm.yml
 
Ens\JobeetBundle\Entity\User:
  type: entity
  table: user
  id:
    id:
      type: integer
      generator: { strategy: AUTO }
  fields:
    username:
      type: string
      length: 255
    password:
      type: string
      length: 255

Maintenant, exécutez la commande doctrine:generate:entities pour créer la nouvelle entité User:

php app/console doctrine:generate:entities EnsJobeetBundle

Et mettez à jour la BDD:

php app/console doctrine:schema:update --force

La seule exigence pour votre nouvelle classe User est qu'elle implémente l'interface UserInterface. Cela signifie que votre concept d'un "utilisateur" peut être n'importe quoi, tant qu'il implémente cette interface. Ouvrez le fichier User.php et modifiez-le comme suit:

// src Ens/JobeetBundle/Entity/User.php
 
namespace Ens\JobeetBundle\Entity;
 
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
 
class User implements UserInterface
{
    private $id;
 
    private $username;
 
    private $password;
 
    public function getId()
    {
        return $this->id;
    }
 
    public function setUsername($username)
    {
        $this->username = $username;
    }
 
    public function getUsername()
    {
        return $this->username;
    }
 
    public function setPassword($password)
    {
        $this->password = $password;
    }
 
    public function getPassword()
    {
        return $this->password;
    }
 
    public function getRoles()
    {
        return array('ROLE_ADMIN');
    }
 
    public function getSalt()
    {
        return null;
    }
 
    public function eraseCredentials()
    {
 
    }
 
    public function equals(UserInterface $user)
    {
        return $user->getUsername() == $this->getUsername();
    }
}

Pour l'entité générée, nous avons ajouté les méthodes requises par la classe UserInterface: GetRoles, getSalt, eraseCredentials et equals.

Ensuite, configurez un fournisseur d'entité utilisateur, et faites-le pointer vers votre classe User:

# app/config/security.yml
# ...
 
    providers:
        main:
            entity: { class: Ens\JobeetBundle\Entity\User, property: username }
 
    encoders:
        Ens\JobeetBundle\Entity\User: sha512

Nous avons également changé le chiffrage pour notre nouvelle classe User afin d'utiliser l'algorithme sha512 pour hasher les mots de passe.

Maintenant, tout est mis en place, mais nous avons besoin de créer notre premier utilisateur. Pour ce faire, nous allons créer une nouvelle commande de Symfony:

// src/Ens/JobeetBundle/Command/JobeetUsersCommand.php
 
namespace Ens\JobeetBundle\Command;
 
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Ens\JobeetBundle\Entity\User;
 
class JobeetUsersCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('ens:jobeet:users')
            ->setDescription('Add Jobeet users')
            ->addArgument('username', InputArgument::REQUIRED, 'The username')
            ->addArgument('password', InputArgument::REQUIRED, 'The password')
        ;
    }
 
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $username = $input->getArgument('username');
        $password = $input->getArgument('password');
 
        $em = $this->getContainer()->get('doctrine')->getEntityManager();
 
        $user = new User();
        $user->setUsername($username);
        // encode the password
        $factory = $this->getContainer()->get('security.encoder_factory');
        $encoder = $factory->getEncoder($user);
        $encodedPassword = $encoder->encodePassword($password, $user->getSalt());
        $user->setPassword($encodedPassword);
        $em->persist($user);
        $em->flush();
 
        $output->writeln(sprintf('Added %s user with password %s', $username, $password));
    }
}

Pour ajouter votre premier utilisateur exécutez:

php app/console ens:jobeet:users admin admin

Cela va créer l'utilisateur admin avec le mot de passe admin. Vous pouvez l'utiliser pour vous connecter à l'admininistration.


Déconnexion

La déconnexion est gérée automatiquement par le pare-feu. Tout ce que vous avez à faire est d'activer le paramètre logout:

# app/config/security.yml
security:
    firewalls:
        secured_area:
            # ...
            logout:
                path:   /logout
                target: /
    # ...

Une fois que cela est configuré dans votre pare-feu, l'envoi d'un utilisateur vers /logout (ou le paramètre path que vous avez configuré), déconnectera l'utilisateur en cours. L'utilisateur sera alors envoyé à la page d'accueil (la valeur définie par le paramètre target).

Vous n'aurez pas besoin de mettre en œuvre un contrôleur pour l'URL /logout puisque le pare-feu s'occupe de tout. Vous pouvez, cependant, vouloir créer une route de sorte que vous pouvez l'utiliser pour générer l'URL:

# src/Ens/JobeetBundle/Resources/config/routing.yml
# ...
 
logout:
    pattern:   /logout
 
# ...

Tout ce qui reste à faire est d'ajouter le lien de déconnexion à notre administration. Pour ce faire, nous allons remplacer le user_block.html.twig de SonataAdminBundle. Créez le fichier user_block.html.twig dans le répertoire app/Resources/SonataAdminBundle/views/Core/:

<!-- app/Resources/SonataAdminBundle/views/Core/user_block.html.twig -->
 
{% block user_block %}<a href="{{ path('logout') }}">Logout</a>{% endblock %}

Maintenant, si vous essayez d'entrer dans l'administration, il vous sera demandé un nom d'utilisateur et un mot de passe, puis, le lien de déconnexion sera affiché dans le coin supérieur droit.


La session utilisateur

Symfony2 fournit un objet de session que vous pouvez utiliser pour stocker des informations sur l'utilisateur entre les requêtes. Par défaut, Symfony2 stocke les attributs dans un cookie par l'intermédiaire des sessions PHP natives.

Vous pouvez stocker et récupérer des informations à partir de la session facilement depuis le contrôleur:

$session = $this->getRequest()->getSession();
 
// store an attribute for reuse during a later user request
$session->set('foo', 'bar');
 
// in another controller for another request
$foo = $session->get('foo');

Malheureusement, les scénarios de Jobeet n'ont aucune exigence qui comprend le stockage d'informations dans la session utilisateur. Nous allons donc ajouter une nouvelle exigence: pour faciliter la navigation dans l'offre, les trois dernières offres vues par l'utilisateur doivent être affichées dans le menu avec des liens pour revenir à la page des offres par la suite.

Lorsqu'un utilisateur accède à la page d'une offre, l'objet de l'offre affichée doit être ajouté dans l'historique de l'utilisateur et stocké dans la session:

// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
public function showAction($id)
{
    $em = $this->getDoctrine()->getEntityManager();
 
    $entity = $em->getRepository('EnsJobeetBundle:Job')->getActiveJob($id);
 
    if (!$entity) {
        throw $this->createNotFoundException('Unable to find Job entity.');
    }
 
    $session = $this->getRequest()->getSession();
 
    // fetch jobs already stored in the job history
    $jobs = $session->get('job_history', array());
 
    // store the job as an array so we can put it in the session and avoid entity serialize errors
    $job = array('id' => $entity->getId(), 'position' =>$entity->getPosition(), 'company' => $entity->getCompany(), 'companyslug' => $entity->getCompanySlug(), 'locationslug' => $entity->getLocationSlug(), 'positionslug' => $entity->getPositionSlug());
 
    if (!in_array($job, $jobs)) {
        // add the current job at the beginning of the array
        array_unshift($jobs, $job);
 
        // store the new job history back into the session
        $session->set('job_history', array_slice($jobs, 0, 3));
    }
 
    $deleteForm = $this->createDeleteForm($id);
 
    return $this->render('EnsJobeetBundle:Job:show.html.twig', array(
        'entity'      => $entity,
        'delete_form' => $deleteForm->createView(),
    ));
}

Dans le layout, ajoutez le code suivant avant la div #content:

<!-- src/End/JobeetBundle/Resources/views/layout.html.twig -->
<!-- ... -->
 
<div id="job_history">
    Recent viewed jobs:
    <ul>
        {% for job in app.session.get('job_history') %}
            <li>
                <a href="{{ path('ens_job_show', { 'id': job.id, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}">{{ job.position }} - {{ job.company }}</a>
            </li>
        {% endfor %}
    </ul>
</div>
 
<div id="content">
 
<!-- ... -->

Les messages flash

Les messages flash sont des petits messages que vous pouvez stocker sur la session de l'utilisateur pendant exactement une requête supplémentaire. Cette fonction est utile lors du traitement d'un formulaire: vous souhaitez rediriger et afficher un message spécial sur la requête suivante. Nous avons déjà utilisé des messages flash dans notre projet lorsque nous publions une offre:

// src/Ens/JobeetBundle/Controller/JobController.php
// ...
 
public function publishAction($token)
{
    // ...
 
    $this->get('session')->setFlash('notice', 'Your job is now online for 30 days.');
 
    // ...
}

Le premier argument de la fonction setFlash est l'identifiant du flash et le second est le message à afficher. Vous pouvez définir les flashes que vous voulez, mais notice et error sont les deux plus communs.

Pour afficher les messages flash à l'utilisateur, vous devez les inclure dans le template. Nous l'avons fait dans le template layout.html.twig:

<!-- src/Ens/JobeetBundle/Resources/views/layout.html.twig -->
<!-- ... -->
 
{% if app.session.hasFlash('notice') %}
    <div>
        {{ app.session.flash('notice') }}
    </div>
{% endif %}
 
<!-- ... -->

Chapitre précédent Chapitre suivant


Une question ? Une réaction ?

comments powered by Disqus