Skip to content

Gérer la connexion (Auth process)

Florent SCHILDKNECHT edited this page Jan 27, 2014 · 1 revision

Le composant Sécurité de Symfony permet de définir des "firewalls", protégant des routes communes.

Définir les firewalls

La gestion des firewalls est contenue dans le fichier app/config/security.yml :

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext # pour les utilisateurs statiques
        Test\Bundle\Entity\TestEntity: # Pour une entité précise
            algorithm:            md5
            encode_as_base64:     false
            iterations:           1

    role_hierarchy:
        ROLE_TEST:        ROLE_USER
        ROLE_ADMIN:       ROLE_TEST
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        test_users: # utilisateurs statiques définis en dur -> Vont utiliser l'encodeur de Symfony\Component\Security\Core\User\User (voir ci-dessus)
            memory:
                users:
                    test_login:
                        password: test_password
                        roles: [ 'ROLE_USER' ]
                    test_login_2:
                        password: test_password_2
                        roles: [ 'ROLE_USER' ]
        database: # utilisateurs venant d'une table en base de donnée
            entity:
                class: Test\Bundle\Entity\TestEntity
                property: name
        cas: # utilisateurs venant d'un service de type AuthenticationProvider personnalisé, faisant appel à une API externe par exemple, le CAS
            id: cas_user_provider

        multi_provider: # Cela permet de combiner plusieurs providers, donc accepter plusieurs sources d'utilisateurs
            chain:
                providers: [static, local, cas]

    firewalls:
        secured_area_login:
            pattern: ^/secure/login$
            anonymous: ~ # Obligatoire, la route de login doit être accessible à tous...

        secured_area:
            pattern: ^/secure
            provider: multi_provider # la clé du provider précédent
            anonymous: false # Autoriser l'anonymat ou non
            form_login:
                login_path: test_bundle_login_page # la route de login, ou bien l'URL direct : /secure/login
                check_path: test_bundle_login_check_page # la route de vérification du login ou bien l'URL direct : /secure/login_check par exemple
                post_only: true # Autoriser la connexion en GET ou non
                remember_me: false # Utilisation de cookies
            logout:
                path: test_bundle_logout_page # la route de déconnexion ou bien l'URL direct : /secure/logout par exemple
                target: / # L'URL de redirection après déconnexion
                invalidate_session: false # Attention, true provoque une erreur 500 PHP (bug d'invalidation des sessions) jusqu'à une certaine version... Et n'est pas requis...

    access_control:
        - { path: ^/secure, roles: ROLE_USER } # Demande que le rôle minimal de l'utilisateur connecté pour les routes commençant par "/secure" soit ROLE_USER

Cette configuration est assez spéciale mais la documentation en ligne de symfony2 est précise : Sécurité Symfony

Utiliser des utilisateurs d'une base de donnée locale

Pour utiliser des utilisateurs locaux, il faut renseigner l'entité comme provider dans la configuration, et l'entité définie doit implémenter l'interface "UserInterface" de Symfony, avec notamment 3 méthodes obligatoires : getUsername() qui renvoie vers la propriété servant de login (pouvant être un login, un email...), eraseCredentials(), servant à je ne sais pas trop quoi et getRoles() définissant les rôles de l'utilisateur (ces rôles peuvent être statiques ou en base de données également !)

Il est également conseillé d'implémenter l'interface native Serializable de PHP avec les 2 méthodes jointes ci-dessous.

<?php

namespace Test\Bundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;

/**
 * TestEntity
 *
 * @ORM\Table(name="test_entity")
 * @ORM\Entity(repositoryClass="Test\Bundle\Entity\TestEntityRepository")
 */
class TestEntity implements UserInterface, \Serializable
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;

    /**
     * @inheritDoc
     */
    public function getUsername()
    {
        return $this->name;
    }

    /**
     * @inheritDoc
     */
    public function getRoles()
    {
        return array('ROLE_USER');
    }

    /**
     * @inheritDoc
     */
    public function eraseCredentials()
    {
    }

    /**
     * @see \Serializable::serialize()
     */
    public function serialize()
    {
        return serialize(array(
            $this->id,
        ));
    }

    /**
     * @see \Serializable::unserialize()
     */
    public function unserialize($serialized)
    {
        list (
            $this->id,
        ) = unserialize($serialized);
    }
}

Enfin, il faut définir un UserProvider, par exemple le Repository de l'entité, implémentant l'interface UserProviderInterface, contenant 2 méthodes obligatoires loadUserByUsername($username) qui cherche l'utilisateur par son login, et refreshUser($user) qui définie les classes d'utilisateur autorisées (pratique en cas de hierarchie) :

<?php

namespace Test\Bundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;

/**
 * TestEntityRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class TestEntityRepository extends EntityRepository implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        $testEntity = null;

        $qb = $this->_em->createQueryBuilder();

        // Requête SQL dans la table "test_entity"
        $qb->select('te')
            ->from('TestBundle:TestEntity', 'te')
            ->where('te.name = :name')
            ->setParameter('name', $username)
        ;

        try {
            $testEntity = $qb->getQuery()->getSingleResult();
        }
        catch (NoResultException $e) {
            throw new UsernameNotFoundException(
                sprintf(
                    'Ce nom d\'utilisateur est inconnu : "%s".',
                    $username
                ), 0, $e
            );
        }

        return $testEntity;
    }

    public function refreshUser(UserInterface $testEntity)
    {
        if (!$this->supportsClass(get_class($testEntity))) {
            throw new UnsupportedUserException(
                sprintf(
                    'Instances of "%s" are not supported.',
                    $testEntity
                )
            );
        }

        return $this->find($testEntity->getId());
    }

    public function supportsClass($class)
    {
        return ($class === $this->getEntityName() || is_subclass_of($class, $this->getEntityName()));
    }
}

Utiliser des ressources externes (API...)

Pour utiliser des utilisateurs externes, il suffit de créer une classe Modèle (qui ne sera cependant pas une entité, il est donc préférable de ne pas l'inclure dans le sous-dossier Entity du bundle !!) implémentant UserInterface, comme dans le cas d'une entité, et un UserProvider implémentant UserProviderInterface...

C'est exactement une entité, mais sans les annotations Doctrine, au final.

Enfin, il faut ajouter ce provider comme un service, car il n'a pas d'encodeur ni de gestion locale :

# Fichier src/Test/Bundle/Resources/config/services.yml
parameters:
    cas_user_provider.class: Test\Bundle\CAS\CASUserProvider # Class du provider

services:
    cas_user_provider:
        class: "%cas_user_provider.class%"

Et PAF ! Ca fait des chocapics... Mais uniquement grâce au génial DependencyInjector de Symfony qui vous permet de définir tous les éléments de ce fichier en tant que services, accessibles de n'importe où... Y compris dans le fichier app/config/security.yml !