Creare un fornitore utenti personalizzato

Parte del processo standard di autenticazione di Symfony dipende dai “fornitori utenti”. Quando un utente invia nome e password, il livello di autenticazione chiede al fornitore utenti configurato di restituire un oggetto utente per un dato nome utente. Symfony quindi verifica che la password di tale utente sia corretta e genera un token di sicurezza, in modo che l’utente resti autenticato per la sessione corrente. Symfony dispone di due fornitori utenti predefiniti, “in_memory” e “entity”. In questa ricetta, vedremo come poter creare il proprio fornitore utenti, che potrebbe essere utile se gli utenti accedono tramite una base dati personalizzata, un file, oppure (come mostrato in questo esempio) tramite un servizio web.

Creare una classe utente

Prima di tutto, indipendentemente dalla provenienza dei dati utente, occorre creare una classe User, che rappresenti tali dati. La classe User, comunque, può essere fatta a piacere e contenere qualsiasi dato si desideri. L’unico requisito è che implementi Symfony\Component\Security\Core\User\UserInterface. I metodi in tale interfaccia vanno quindi definiti nella classe utente personalizzata: getRoles(), getPassword(), getSalt(), getUsername(), eraseCredentials(). Potrebbe essere utile anche implementare l’interfaccia Symfony\Component\Security\Core\User\EquatableInterface, che definisce un metodo per verificare se l’utente corrisponde all’utente corrente. Tale interfaccia richiede un metodo isEqualTo().

Ecco la class WebserviceUser in azione:

// src/Acme/WebserviceUserBundle/Security/User/WebserviceUser.php
namespace Acme\WebserviceUserBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

class WebserviceUser implements UserInterface, EquatableInterface
{
    private $username;
    private $password;
    private $salt;
    private $roles;

    public function __construct($username, $password, $salt, array $roles)
    {
        $this->username = $username;
        $this->password = $password;
        $this->salt = $salt;
        $this->roles = $roles;
    }

    public function getRoles()
    {
        return $this->roles;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getSalt()
    {
        return $this->salt;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function eraseCredentials()
    {
    }

    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            return false;
        }

        if ($this->password !== $user->getPassword()) {
            return false;
        }

        if ($this->getSalt() !== $user->getSalt()) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
    }
}

Se si hanno maggiori informazioni sui propri utenti, come il nome di battesimo, si possono aggiungere campi per memorizzare tali dati.

Creare un fornitore utenti

Ora che abbiamo una classe User, creeremo un fornitore di utenti, che estrarrà informazioni da un servizio web, creerà un oggetto WebserviceUser e lo popolerà con i dati.

Il fornitore utenti è semplicemente una classe PHP che deve implementare Symfony\Component\Security\Core\User\UserProviderInterface, la quale richiede la definizione di tre metodi: loadUserByUsername($username), refreshUser(UserInterface $user) e supportsClass($class). Per maggiori dettagli, vedere Symfony\Component\Security\Core\User\UserProviderInterface.

Ecco un esempio di come potrebbe essere:

// src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php
namespace Acme\WebserviceUserBundle\Security\User;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;

class WebserviceUserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        // fare qui una chiamata al servizio web
        $userData = ...
        // supponiamo che restituisca un array, oppure false se non trova utenti

        if ($userData) {
            $password = '...';

            // ...

            return new WebserviceUser($username, $password, $salt, $roles);
        }

        throw new UsernameNotFoundException(
            sprintf('Nome utente "%s" non trovato.', $username)
        );
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            throw new UnsupportedUserException(
                sprintf('Istanza di "%s" non supportata.', get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'Acme\WebserviceUserBundle\Security\User\WebserviceUser';
    }
}

Creare un servizio per il fornitore utenti

Ora renderemo il fornitore utenti disponibile come servizio.

  • YAML
    # src/Acme/WebserviceUserBundle/Resources/config/services.yml
    services:
        webservice_user_provider:
            class: Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider
    
  • XML
    <!-- src/Acme/WebserviceUserBundle/Resources/config/services.xml -->
    <services>
        <service id="webservice_user_provider" class="Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider" />
    </services>
    
  • PHP
    // src/Acme/WebserviceUserBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setDefinition(
        'webservice_user_provider',
        new Definition('Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider')
    );
    

Suggerimento

La vera implementazione del fornitore utenti avrà probabilmente alcune dipendenze da opzioni di configurazione o altri servizi. Aggiungerli come parametri nella definizione del servizio.

Nota

Assicurarsi che il file dei servizi sia importato. Vedere Importare la configurazione con imports per maggiori dettagli.

Modificare security.yml

È tutto in /app/config/security.yml. Aggiungere il fornitore di utenti alla lista di fornitori nella sezione “security”. Scegliere un nome per il fornitore di utenti (p.e. “webservice”) e menzionare l’id del servizio appena definito.

  • YAML
    # app/config/security.yml
    security:
        providers:
            webservice:
                id: webservice_user_provider
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <provider name="webservice" id="webservice_user_provider" />
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'webservice' => array(
                'id' => 'webservice_user_provider',
            ),
        ),
    ));
    

Symfony deve anche sapere come codificare le password fornite dagli utenti, per esempio quando compilano il form di login. Lo si può fare aggiungendo una riga alla sezione “encoders”, in /app/config/security.yml.

  • YAML
    # app/config/security.yml
    security:
        encoders:
            Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <encoder class="Acme\WebserviceUserBundle\Security\User\WebserviceUser">sha512</encoder>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'encoders' => array(
            'Acme\WebserviceUserBundle\Security\User\WebserviceUser' => 'sha512',
        ),
    ));
    

Il valore inserito deve corrispondere al modo in cui le password sono state codificate originariamente, alla creazione degli utenti (in qualsiasi modo siano stati creati). Quando un utente inserisce la sua password, la password viene concatenata con il valore del sale e quindi codificata con questo algoritmo, prima di confrontarla con la password restituita dal proprio metodo getPassword(). Inoltre, a seconda delle proprie opzioni, la password può essere codificata più volte e poi codificata in base64.