Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Autenticazione con Facebook in applicazioni Symfony

Come usare Facebook per autenticare l'utente in un'applicazione Web sviluppata con Symfony. Descrizione ed esempi pratici d'uso del FOSFacebookBundle per PHP
Come usare Facebook per autenticare l'utente in un'applicazione Web sviluppata con Symfony. Descrizione ed esempi pratici d'uso del FOSFacebookBundle per PHP
Link copiato negli appunti

Un'applicazione web nella gran parte dei casi prevede un meccanismo di registrazione e login. Rifacendosi al concetto dell'easy in/easy out, e tenendo presente che l'utente non è di certo felice di eseguire l'ennesima registrazione con tanto di nuova password da ricordare per utilizzare il nostro servizio, è consigliabile sfruttare quanto messo a disposizione dai maggiori social network in termini di autenticazione. In particolare quelle del più diffuso social network al mondo: Facebook.

Mettere gli utenti nelle condizioni di accedere alla nostra applicazione sfruttando la loro registrazione a Facebook permette all'applicazione stessa di avere già una base utenti potenziale di oltre 800 milioni di persone.

In questo articolo parleremo di come sfruttare l'autenticazione di Facebook sulle  applicazioni Symfony utilizzando il bundle FOSFacebookBundle rispettando il presupposto di non reinventare la ruota sfruttando una soluzione messa a disposizione dalla comunità di sviluppatori nata attorno a Symfony.

Nota: la seguente è una traduzione del file README ufficiale di del Bundle FOSFacebookBundle integrata da nostre annotazioni. Si pubblica in questa sede con rispetto della licenza.

Introduzione

Questo bundle semplifica l'integrazione degli SDK PHP e JS di Facebook in un'applicazione Symfony2. Tra le altre cose mette a disposizione un authentication provider che permette agli utenti di eseguire il login su un'applicazione Symfony2 tramite Facebook. Inoltre, sfruttando il supporto per i custom user provider, il login via Facebook può essere integrato con altre soluzioni di autenticazione come quello offerto da un altro bundle molto utile come il FOSUserBundle.

Autenticare un utente richiede l'esecuzione di alcuni passaggi:

  1. L'utente deve essere autenticato su Facebook
  2. L'utente deve connettere il proprio account Facebook al sito in esame
  3. Una volta che l'utente ha eseguito i passaggi ai punti 1 e 2 va eseguito il login

Per i passaggi 1 e 2 descritti sopra esistono due opzioni:

  1. Selezionare OAuth Migration nelle impostazione dell'applicazione Facebook.
  2. Aggiungere un pulsante di login di Facebook, questo approccio richiede del codice JS per gestire il passaggio 3.
  3. Permettere al bundle FOSFacebookBundle di rindirizzare l'utente alla pagina di login di Facebook.

Da notare che l'ultima condizione si verifica automaticamente se il primo provider nel primo firewall configurato è associato al FOSFacebookBundle e l'utente accede ad una pagina che richiede autenticazione senza essere autenticato.

Per maggiore documentazione relativa alle applicazione Facebook si può fare riferimento alla documentazione ufficiale di Facebook. Fare riferimento anche alla documentazione ufficiale del SecurityBundle di Symfony, specialmente per i dettagli relativi alla configurazione.

Installazione

Tutti i passaggi della seguente guida vengono eseguiti su una Symfony Standard Edition che potete scaricare dal sito di Symfony. La scelta tra la versione con vendor o senza di essi dipende dal grado di flessibilità che si vuole per il progetto. Scaricare la versione senza vendor è la scelta ottimale nel caso in cui si abbia a disposizione Git sulla macchina utilizzata per lo sviluppo e si decida inoltre di mettere il progetto sotto controllo di versione (con Git, Svn, ...); in questo modo i componenti di Symfony non verranno inseriti direttamente nel repository ma verranno richiamati esternamente.

La versione completa di vendor è pronta all'uso più velocemente ed è consigliata agli utenti meno esperti dato che richiede minori operazioni di setup.

Passo 1

Aggiungere il bundle e l'SDK PHP di Facebook alla directory vendor/:

1.1: Utilizzando lo script per i vendor.

Aggiungere le seguenti righe al file deps:

[FOSFacebookBundle]
    git=git://github.com/FriendsOfSymfony/FOSFacebookBundle.git
    target=/bundles/FOS/FacebookBundle
[FacebookSDK]
    git=git://github.com/facebook/php-sdk.git
    target=/facebook

Aggiungere la seguente riga al file deps.lock (al momento di scrittura di questa guida la tag 1.0.0 rappresenta la versione funzionante del bundle):

FOSFacebookBundle b3bfbfc2898d62ccb3ec697cafe32f374928d49c

Eseguire lo script dei vendor:

./bin/vendors install

1.2: Utilizzando i git submodules.

$ git submodule add git://github.com/FriendsOfSymfony/FOSFacebookBundle.git vendor/bundles/FOS/FacebookBundle

$ git submodule add git://github.com/facebook/php-sdk.git vendor/facebook

Passo 2

Aggiungere il namespace FOS all'autoloader:

// app/autoload.php
$loader->registerNamespaces(array(
    'FOS' => __DIR__.'/../vendor/bundles',
    // your other namespaces ));

Passo 3

Aggiungere il bundle al kernel dell'applicazione:

// app/ApplicationKernel.php
public function registerBundles()
{
    return array(
        // ...
        new FOSFacebookBundleFOSFacebookBundle(),
        // ...
    );
}

Passo 4

Aggiungere le seguenti rotte all'applicazione facendo in modo di farle puntare alle action dell'attuale controller:

#application/config/routing.yml
_security_check:
    pattern: /login_check _security_logout:
    pattern: /logout #application/config/routing.xml
<route id="_security_check" pattern="/login_check" />
<route id="_security_logout" pattern="/logout" />

Passo 5

Configurare il servizio facebook nel file di configurazione:

# application/config/config.yml
    fos_facebook:
    file: %kernel.root_dir%/../vendor/facebook/src/base_facebook.php
    alias: facebook
    app_id: 123456879
    secret: s3cr3t
    cookie: true
    permissions: [email, user_birthday, user_location]
# application/config/config.xml
<fos_facebook:api
    file="%kernel.root_dir%/../vendor/facebook/src/base_facebook.php"
    alias="facebook"
    app_id="123456879"
    secret="s3cr3t"
    cookie="true"
>
        <permission>email</permission>
        <permission>user_birthday</permission>
        <permission>user_location</permission>
</fos_facebook:api>

Se non viene incluso un valore per la chiave file nella configurazione sarà necessario configurare l'applicazione per fare in modo che possa eseguire l'autoload della classe BaseFacebook.

Passo 6

La seguente configurazione deve essere aggiunta nel caso in cui si voglia utilizzare il security component:

# application/config/config.yml
security:
    factories:
        - "%kernel.root_dir%/../vendor/bundles/FOS/FacebookBundle/Resources/config/security_factories.xml"
    providers:
        fos_facebook:
            id: fos_facebook.auth
    firewalls:
        public:
            # dal momento che gli utenti anonimi sono accettati non forziamo il login
            pattern: ^/.*
            fos_facebook:
                app_url: "http://apps.facebook.com/appName/"
                server_url: "http://localhost/facebookApp/"
            anonymous: true
            logout:
                handlers: ["fos_facebook.logout_handler"]
    access_control:
        - { path: ^/secured/.*, role: [IS_AUTHENTICATED_FULLY] } # Questa è la rotta protetta da fos_facebook
        - { path: ^/.*, role: [IS_AUTHENTICATED_ANONYMOUSLY] }

Sarà necessario aggiungere /secured/ nella configurazione perché questo funzioni. Una configurazione d'esempio è la seguente:

_facebook_secured:
    pattern: /secured/
    defaults: { _controller: AcmeDemoBundle:Welcome:index }

Passo 7

A scelta è possibile definire una classe da utilizzare come custom user provider o definire il path per il login

# application/config/config.yml
security:
    factories:
        - "%kernel.root_dir%/../vendor/bundles/FOS/FacebookBundle/Resources/config/security_factories.xml"
    providers:
        # scegli liberamente il nome del provide
        my_fos_facebook_provider:
            id: my.facebook.user # vedere "Esempio di Customer User Provider utilizzando il bundle FOSUserBundle" a pagina 4
    firewalls:
        public:
            pattern: ^/.*
            fos_facebook:
                app_url: "http://apps.facebook.com/appName/"
                server_url: "http://localhost/facebookApp/"
                login_path: ^/login
                check_path: ^/login_check$
                default_target_path: /
                provider: my_fos_facebook_provider
            anonymous: true
            logout:
                handlers: ["fos_facebook.logout_handler"]
# application/config/config_dev.yml
    security:
        firewalls:
            public:
                fos_facebook:
                    app_url: "http://apps.facebook.com/appName/"
                    server_url: "http://localhost/facebookApp/app_dev.php/"

Passo 8

A scelta utilizzare il controllo degli accessi per proteggere URL specifici

# application/config/config.yml
security:
    # ...
    access_control:
        - { path: ^/facebook/, role: [ROLE_FACEBOOK] }
        - { path: ^/.*, role: [IS_AUTHENTICATED_ANONYMOUSLY] }

Il ruolo ROLE_FACEBOOK deve essere aggiunto nella classe User (fare riferimento alla classe AcmeMyBundleEntityUser::setFBData() che vedremo in seguito).

Nota bene: l'ordine delle regole per il controllo degli accessi influisce sul loro funzionamento.

Impostare l'SDK JavaScript

È disponibile un helper per automatizzare il caricamento dell'SDK JavaScript di Facebook e la sua inizializzazione utilizzando i parametri recuperati dal service container. Per utilizzare l'ambiente JavaScript di Facebook sarà necessario aggiungere il seguente codice al layout dell'applicazione subito dopo l'apertura del tag body:

 <body>
    <!-- all'interno di un template php -->
    <?php echo $view['facebook']->initialize(array('xfbml' => true, 'fbAsyncInit' => 'onFbInit();')) ?>
    <!-- all'interno di un template twig -->
    {{ facebook_initialize({'xfbml': true, 'fbAsyncInit': 'onFbInit();'}) }}

Da notare che fbAsyncInit è un parametro necessario per l'esecuzione del codice JavaScript della funzione che si occupa di inizializzare la connessione con Facebook subito dopo la chiamata FB.init();. La funzione onFbInit(); è necessaria per l'esecuzione di altre funzioni che richiedono FB inizializzato.

Nel caso in cui si decida di utilizzare il markup XFBML nella nostra applicazione sarà necessario dichiarare il namespace corretto nell'apertura del tag html:

<html xmlns:fb="http://www.facebook.com/2008/fbml">

Aggiungere il pulsante di login nei template

Basterà semplicemente aggiungere il codice seguente in uno dei template dell'applicazione:

<!-- all'interno di un template php -->
<?php echo $view['facebook']->loginButton(array('autologoutlink' => true)) ?>

<!-- all'interno di un template twig -->
{{ facebook_login_button({'autologoutlink': true}) }}

Da notare che con questo approccio verranno gestiti solamente il login e la connessione con Facebook. L'azione di autenticare l'utente nella nostra applicazione Symfony2 deve essere ancora invocata. Per fare questo nella maggior parte dei casi sarà necessario ascoltare l'evento auth.login e successivamente eseguire il redirect al check_path:

<script>
    function goLogIn(){
        window.location = "{{ path('_security_check') }}";
    }
    function onFbInit() {
        if (typeof(FB) != 'undefined' && FB != null ) {
            FB.Event.subscribe('auth.login', function(response) {
                setTimeout(goLogIn,500);
            });
        }
    }
</script>

Una nota di riguardo relativamente al fatto che si imposti un'attesa di 500ms prima di eseguire il redirect per permettere al browser di comunicare con il cookie di Facebook. Evitando questo passaggio si potrà incorrere nel seguente messaggio di errore:

*"L'utente Facebook non può essere recuperato dalla sessione."*

La rotta _security_check dovrà puntare al pattern /login_check per corrispondere alla configurazione vista in precedenza.

Sarà inoltre necessario richiamare l'azione di logout restando in ascolto dell'evento auth.logout per effettuare il redirect alla rotta logout:

<script>
    function goLogIn(){
        window.location = "{{ path('_security_check') }}";
    }
    function onFbInit() {
        if (typeof(FB) != 'undefined' && FB != null ) {
            FB.Event.subscribe('auth.login', function(response) {
                setTimeout(goLogIn,500);
            });
            FB.Event.subscribe('auth.logout', function(response) {
                window.location = "{{ path('_security_logout') }}";
            });
        }
    }
</script>

Esempio di Customer User Provider utilizzando il bundle FOSUserBundle

Il FOSUserBundle è un altro bundle sviluppato dalla comunità che permette di integrare in un'applicazione sviluppata con Symfony2 un sistema di gestione utenti basato su database. Il bundle mette a disposizione un framework molto flessibile per la gestione degli utenti con lo scopo di gestire le operazioni più comuni come il login, la registrazione ed il recupero della password smarrita.

Alcune feature incluse in FOSUserBundle:

  • Gli utenti possono essere memorizzati utilizzando Doctrine ORM, MondoDB/CouchDB ODM  oppure Propel
  • Supporto per il processo di registrazione con servizio di conferma via email opzionale
  • Supporto per il recupero delle password smarrite

Nel caso si volesse utilizzare anche il bundle FOSUserBundle per la gestione di altri sistemi di accesso/gestione degli utenti sarà necessario aggiungere un servizio per il custom user provider da impostare nella sezione "provider" del file config.yml:

services:
    my.facebook.user:
        class: AcmeMyBundleSecurityUserProviderFacebookProvider
        arguments:
            facebook: "@fos_facebook.api"
            userManager: "@fos_user.user_manager"
            validator: "@validator"
            container: "@service_container"
<?php
namespace AcmeMyBundleSecurityUserProvider;
use SymfonyComponentSecurityCoreExceptionUsernameNotFoundException;
use SymfonyComponentSecurityCoreExceptionUnsupportedUserException;
use SymfonyComponentSecurityCoreUserUserProviderInterface;
use SymfonyComponentSecurityCoreUserUserInterface;
use BaseFacebook;
use FacebookApiException;
class FacebookProvider implements UserProviderInterface
{
    /**
    * @var Facebook
    */
    protected $facebook;
    protected $userManager;
    protected $validator;
    public function __construct(BaseFacebook $facebook, $userManager, $validator)
    {
        $this->facebook = $facebook;
        $this->userManager = $userManager;
        $this->validator = $validator;
    }
    public function supportsClass($class)
    {
        return $this->userManager->supportsClass($class);
    }
    public function findUserByFbId($fbId)
    {
        return $this->userManager->findUserBy(array('facebookID' => $fbId));
    }
    public function loadUserByUsername($username)
    {
        $user = $this->findUserByFbId($username);
        try {
            $fbdata = $this->facebook->api('/me');
        } catch (FacebookApiException $e) {
            $fbdata = null;
        }
        if (!empty($fbdata)) {
            if (empty($user)) {
                $user = $this->userManager->createUser();
                $user->setEnabled(true);
                $user->setPassword('');
                $user->setAlgorithm('');
            }
            // TODO use http://developers.facebook.com/docs/api/realtime
            $user->setFBData($fbdata);
            if (count($this->validator->validate($user, 'Facebook'))) {
                // TODO: the user was found obviously, but doesnt match our expectations, do something smart
                throw new UsernameNotFoundException('The facebook user could not be stored');
            }
            $this->userManager->updateUser($user);
        }
        if (empty($user)) {
            throw new UsernameNotFoundException('The user is not authenticated on facebook');
        }
        return $user;
    }
    public function refreshUser(UserInterface $user)
    {
        if (!$this->supportsClass(get_class($user)) || !$user->getFacebookId()) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
    }
        return $this->loadUserByUsername($user->getFacebookId());
    }
}

Infine sarà necessario aggiungere i metodi getFacebookId() e setFBData() al modello User. Il seguente esempio aggiunge anche le proprietà firstname e lastname:

<?php
namespace AcmeMyBundleEntity;
use FOSUserBundleEntityUser as BaseUser;
class User extends BaseUser
{
    /**
    * @var string
    */
    protected $firstname;
    /**
    * @var string
    */
    protected $lastname;
    /**
    * @var string
    */
    protected $facebookID;
    public function serialize()
    {
        return serialize(array($this->facebookId, parent::serialize()));
    }
    public function unserialize($data)
    {
        list($this->facebookId, $parentData) = unserialize($data);
        parent::unserialize($parentData);
    }
    /**
    * @return string
    */
    public function getFirstname()
    {
        return $this->firstname;
    }
    /**
    * @param string $firstname
    */
    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;
    }
    /**
    * @return string
    */
    public function getLastname()
    {
        return $this->lastname;
    }
    /**
    * @param string $lastname
    */
    public function setLastname($lastname)
    {
        $this->lastname = $lastname;
    }
    /**
    * Get the full name of the user (first + last name)
    * @return string
    */
    public function getFullName()
    {
        return $this->getFirstName() . ' ' . $this->getLastname();
    }
    /**
    * @param string $facebookID
    * @return void
    */
    public function setFacebookID($facebookID)
    {
        $this->facebookID = $facebookID;
        $this->setUsername($facebookID);
        $this->salt = '';
    }
    /**
    * @return string
    */
    public function getFacebookID()
    {
        return $this->facebookID;
    }
    /**
    * @param Array
    */
    public function setFBData($fbdata)
    {
        if (isset($fbdata['id'])) {
            $this->setFacebookID($fbdata['id']);
            $this->addRole('ROLE_FACEBOOK');
        }
        if (isset($fbdata['first_name'])) {
            $this->setFirstname($fbdata['first_name']);
        }
        if (isset($fbdata['last_name'])) {
            $this->setLastname($fbdata['last_name']);
        }
        if (isset($fbdata['email'])) {
            $this->setEmail($fbdata['email']);
        }
    }
}

Conclusione

Abbiamo visto come integrare l'accesso ad un'applicazione Symfony2 sfruttando il bundle FOSFacebookBundle sviluppato dalla comunità. Utilizzare soluzioni già sviluppate e testate è alla base della politica di non reinventare la ruota: il tempo risparmiato per la risoluzione di questo problema può essere utilizzato per focalizzare il nostro lavoro su altre parti di business logic dell'applicazione che sicuramente aggiungeranno maggiore valore all'applicazione.

Ti consigliamo anche