Sicurezza

Il sistema di sicurezza di Symfony è incredibilmente potente, ma può anche essere difficile da configurare. In questo capitolo si vedrà come impostare passo-passo la sicurezza di un’applicazione, dalla configurazione del firewall a come caricare utenti per negare l’accesso e recuperare un oggetto utente. A seconda dei bisogni, a volte la prima configurazione potrebbe essere difficoltosa. Ma, una volta a posto, il sistema di sicurezza di Symfony sarà flessibile e (speriamo) divertente.

Questa guida è divisa in alcune sezioni:

  1. Preparazione di security.yml (autenticazione);
  2. Negare l’accesso all’applicazione (autorizzazione);
  3. Recuperare l’oggetto corrispondente all’utente corrente

Successivamente ci saranno un certo numero di piccole (ma interessanti) sezioni, come logout e codifica delle password.

1) Preparazione di security.yml (autenticazione)

Il sistema di sicurezza è configurato in app/config/security.yml. La configurazione predefinita è simile a questa:

  • YAML
    # app/config/security.yml
    security:
        providers:
            in_memory:
                memory: ~
    
        firewalls:
            dev:
                pattern: ^/(_(profiler|wdt)|css|images|js)/
                security: false
    
            default:
                anonymous: ~
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="in_memory">
                <memory />
            </provider>
    
            <firewall name="dev"
                pattern="^/(_(profiler|wdt)|css|images|js)/"
                security=false />
    
            <firewall name="default">
                <anonymous />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'in_memory' => array(
                'memory' => array(),
            ),
        ),
        'firewalls' => array(
            'dev' => array(
                'pattern'    => '^/(_(profiler|wdt)|css|images|js)/',
                'security'   => false,
            ),
            'default' => array(
                'anonymous'  => null,
            ),
        ),
    ));
    

La voce firewalls è il cuore della configurazione della sicurezza. Il firewall dev non è importante, serve solo ad assicurarsi che gli strumenti di sviluppo di Symfony, che si trovano sotto URL come /_profiler e /_wdt, non siano bloccati.

Suggerimento

Si può anche far corrispondere la richiesta ad altri dettagli (p.e. l’host). Per maggiori informazioni ed esempi, leggere Limitare firewall a una specifica richiesta.

Tutti gli altri URL saranno gestiti dal firewall default (l’assenza della chiave pattern vuol dire che corrisponde a ogni URL). Si può pensare al firewall come il proprio sistema di sicurezza e quindi solitamente ha senso avere un singolo firewall. Ma questo non vuol dire che ogni URL richieda autenticazione e quindi la voce anonymous si occupa di questo. In effetti, se ora si apre l’homepage, si potrà accedere e si vedrà che si è “autenticati” come anon.. Non lasciarsi ingannare dalla parola “Yes” vicino ad “Authenticated”, si è ancora un utente anonimo:

../_images/security_anonymous_wdt.png

Più avanti si vedrà come negare l’accesso ad alcuni URL o controllori.

Suggerimento

La sicurezza è altamente configurabile e c’è una guida di riferimento alla configurazione della sicurezza, che mostra tutte le opzioni, con spiegazioni aggiuntive.

A) Configurare il modo in cui gli utenti si autenticano

Il lavoro principale di un firewall è quello di configurare il modo in cui gli utenti si autenticheranno. Useranno un form? Http Basic? Il token di un’API? Tutti questi metodi insieme?

Iniziamo con Http Basic (il caro vecchio popup). Per attivarlo, aggiungere la voce http_basic nel firewall:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        firewalls:
            # ...
            default:
                anonymous: ~
                http_basic: ~
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="default">
                <anonymous />
                <http-basic />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'firewalls' => array(
            // ...
            'default' => array(
                'anonymous'  => null,
                'http_basic' => null,
            ),
        ),
    ));
    

Facile! Per fare una prova, si deve richiedere che un utente sia connesso per poter vedere una pagina. Per rendere le cose interessanti, creare una nuova pagina su /admin. Per esempio, se si usano le annotazioni, creare qualcosa come questo:

// src/AppBundle/Controller/DefaultController.php
// ...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    /**
     * @Route("/admin")
     */
    public function adminAction()
    {
        return new Response('Pagina admin!');
    }
}

Quindi, aggiungere a security.yml una voce access_control che richieda all’utente di essere connesso per poter accedere a tale URL:

  • YAML
    # app/config/security.yml
    security:
        # ...
        firewalls:
            # ...
    
        access_control:
            # require ROLE_ADMIN for /admin*
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="default">
                <!-- ... -->
            </firewall>
    
            <access-control>
                <!-- require ROLE_ADMIN for /admin* -->
                <rule path="^/admin" role="ROLE_ADMIN" />
            </access-control>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'firewalls' => array(
            // ...
            'default' => array(
                // ...
            ),
        ),
       'access_control' => array(
           // require ROLE_ADMIN for /admin*
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

Nota

La questione ROLE_ADMIN e l’accesso negato saranno analizzati più avanti, nella sezione 2) Accesso negato, ruoli e altre autorizzazioni.

Ottimo! Ora, se si va su /admin, si vedrà il popup HTTP Basic:

../_images/security_http_basic_popup.png

Ma come si può entrare? Da dove vengono gli utenti?

Suggerimento

E se invece si volesse un form di login tradizionoale? Nessun problema! Vedere Costruire un form di login tradizionale. Che altri metodi sono supportati? Vedere riferimento sulla configurazione oppure costruire un proprio.

B) Configurare come vengono caricati gli utenti

Quando si inserisce il proprio nome utente, Symfony deve caricare le informazioni da qualche parte. Questo viene chiamato “fornitore di utenti” ed è compito dello sviluppatore configurarlo. Symfony ha un modo predefinito di caricare utenti dalla base dati, ma si può anche creare il proprio fornitore di utenti.

Il modo più facile (ma anche più limitato) è di configurare Symfony per caricare utenti inseriti direttamente nel file security.yml. Questo fornitore è chiamato “in memoria”, ma è meglio pensare a esso come fornitore “in configurazione”:

  • YAML
    # app/config/security.yml
    security:
        providers:
            in_memory:
                memory:
                    users:
                        ryan:
                            password: ryanpass
                            roles: 'ROLE_USER'
                        admin:
                            password: kitten
                            roles: 'ROLE_ADMIN'
        # ...
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="in_memory">
                <memory>
                    <user name="ryan" password="ryanpass" roles="ROLE_USER" />
                    <user name="admin" password="kitten" roles="ROLE_ADMIN" />
                </memory>
            </provider>
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array(
                            'password' => 'ryanpass',
                            'roles' => 'ROLE_USER',
                        ),
                        'admin' => array(
                            'password' => 'kitten',
                            'roles' => 'ROLE_ADMIN',
                        ),
                    ),
                ),
            ),
        ),
        // ...
    ));
    

Come per i firewalls, si possono avere più providers, ma probabilmente ne basterà uno solo. Se si ha bisogno di più fornitori, si può configurare il fornitore usato dal firewall, sotto la voce provider (p.e. provider: in_memory).

Provare a entrare con nome utente admin e password kitten. Si dovrebbe vedere un errore!

No encoder has been configured for account “SymfonyComponentSecurityCoreUserUser”

Per risolvere, aggiungere una chiave encoders:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        encoders:
            Symfony\Component\Security\Core\User\User: plaintext
        # ...
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <encoder class="Symfony\Component\Security\Core\User\User"
                algorithm="plaintext" />
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => 'plaintext',
        ),
        // ...
    ));
    

I fornitori di utenti caricano le informazioni dell’utente e le inseriscono in un oggetto User. Se si caricano utenti dalla base dati o da altre sorgenti, si userà una propria classe personalizzata. Se invece si usa il fornitore “in memoria”, esso restituirà un oggetto Symfony\Component\Security\Core\User\User.

Qualunque sia la classe di User, si deve dire a Symfony quale algoritmo sia stato usato per codificare le password. In questo caso, le password sono in chiaro, ma tra un attimo faremo in modo di usare bcrypt.

Se ora si aggiorna, ci si troverà dentro! La barra di debug del web fornirà informazioni sul nome dell’utente e sui suoi ruoli:

../_images/symfony_loggedin_wdt.png

Poiché questo URL richiede ROLE_ADMIN, se si prova a entrare come ryan ci si vedrà negato l’accesso. Lo vedremo più avanti (Proteggere schemi di URL (access_control)).

Caricare utenti dalla base dati

Se si vogliono caricare gli utenti usando l’ORM di Doctrine, è facile! Vedere Caricare gli utenti dalla base dati (il fornitore di entità) per tutti i dettagli.

C) Codifica delle password

Che gli utenti siano dentro a security.yml, in una base dati o da qualsiasi altra parte, se ne vorranno codificare le password. Il miglior algoritmo da usare è bcrypt:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        encoders:
            Symfony\Component\Security\Core\User\User:
                algorithm: bcrypt
                cost: 12
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <encoder class="Symfony\Component\Security\Core\User\User"
                algorithm="bcrypt"
                cost="12" />
    
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
    
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => array(
                'algorithm' => 'bcrypt',
                'cost' => 12,
            )
        ),
        // ...
    ));
    

Attenzione

Se si usa PHP 5.4 o precedenti, occorrerà installare la libreria ircmaxell/password-compat tramite Composer, per poter usare il codificatore bcrypt:

{
    "require": {
        ...
        "ircmaxell/password-compat": "~1.0.3"
    }
}

Ovviamente, sarà ora necessario codificare le password con tale algoritmo. Per gli utenti inseriti a mano, si può usare uno strumento online, che restituirà qualcosa del genere:

  • YAML
    # app/config/security.yml
    security:
        # ...
    
        providers:
            in_memory:
                memory:
                    users:
                        ryan:
                            password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli
                            roles: 'ROLE_USER'
                        admin:
                            password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G
                            roles: 'ROLE_ADMIN'
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <provider name="in_memory">
                <memory>
                    <user name="ryan" password="$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli" roles="ROLE_USER" />
                    <user name="admin" password="$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G" roles="ROLE_ADMIN" />
                </memory>
            </provider>
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array(
                            'password' => '$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli',
                            'roles' => 'ROLE_USER',
                        ),
                        'admin' => array(
                            'password' => '$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G',
                            'roles' => 'ROLE_ADMIN',
                        ),
                    ),
                ),
            ),
        ),
        // ...
    ));
    

Tutto funzionerà come prima. Ma se si hanno utenti dinamici (p.e. da base dati), come si fa a codificare ogni password prima dell’inserimento? Nessun problema, vedere Codifica dinamica di una password per i dettagli.

Suggerimento

Gli algoritmi supportati dipendono dalla versione di PHP, ma includono gli algoritmi restituiti dalla funzione hash_algos, più alcuni altri (come bcrypt). Vedere la voce encoders nella sezione riferimento della sicurezza per degli esempi.

Si possono anche usare algoritmi differenti per singolo utente. Vedere Scegliere un algoritmo dinamico per la codifica di password per maggiori dettagli.

D) Configurazione conclusa!

Congratulazioni! Ora di dispone un sistema di autenticazione funzionante, che usa Http Basic e carica utenti dal file security.yml.

I prossimi passi possono variare:

2) Accesso negato, ruoli e altre autorizzazioni

Ora gli utenti possono accedere all’applicazione usando http_basic o un altro metodo. Ottimo! Ora, occorre imparare come negare l’accesso e lavorare con l’oggetto User. Questo processo prende il nome di autorizzazione e spetta a esso decidere se un utente possa accedere a una determinata risorsa (un URL, un oggetto del modello, una chiamata a un metodo, ...).

Il processo di autorizzazione ha due lati:

  1. L’utente riceve uno specifico insieme di ruoli, quando entra (p.e. ROLE_ADMIN).
  2. Si aggiunge codice in modo che una risorsa (come un URL o un controllore) richieda uno specifico “attributo” (solitamente un ruolo, come ROLE_ADMIN) per potervi accedere.

Suggerimento

Oltre ai ruoli (come ROLE_ADMIN), si può proteggere una risorsa tramite altri attributi/stringhe (come EDIT) e usare i votanti o il sistema ACL di Symfony per dar loro un significato. Questo può essere utile nel caso serva verificare se l’utente A possa modificare un oggetto B (p.e. un prodotto con un determinato id). Vedere Access Control List (ACL): proteggere singoli oggetti della base dati.

Ruoli

Quando un utente entra, riceve un insieme di ruoli (p.e. ROLE_ADMIN). Nell’esempio precedente, tali ruoli sono scritti a mano in security.yml. Se si caricano utenti dalla base dati, probabilmente saranno memorizzati in una colonna della tabella.

Attenzione

Tutti i ruoli assegnati devono avere il prefisso ROLE_. In caso contrario, non possono essere gestiti dal sistema di sicurezza di Symfony (a meno che non si faccia qualcosa di avanzato, assegnare un ruolo come PIPPO a un utente e poi verificare PIPPO, come descritto successivamente non funzionerà).

I ruoli sono semplici e sono di base stringhe inventate e usate come necessario. Per esempio, per poter iniziare a limitare l’accesso alla sezione amministrativa di un blog, si può proteggere tale sezione usando un ruolo ROLE_BLOG_ADMIN. Non occorre definire tale ruolo in altri posti, basta iniziare a usarlo.

Suggerimento

Assicurarsi che ciascun utente abbia almeno un ruolo, altrimenti sembrerà che l’utente non sia autenticato. Una convenzione tipica consiste nell’assegnare a ogni utente ROLE_USER.

Si può anche specificare una gerarchia di ruoli, in cui determinati ruoli ne hanno automaticamente anche altri.

Aggiungere codice per negare l’accesso

Ci sono due modi per negare accesso a qualcosa:

  1. access_control in security.yml consente di proteggere schemi di URL (p.e. /admin/*). È facile, ma meno flessibile;
  2. nel codice, tramite il servizio security.context.

Proteggere schemi di URL (access_control)

Il modo più semplice per proteggere parti di un’applicazione è proteggere un intero schema di URL. L’abbiamo visto in precedenza, quando abbiamo richiesto che ogni URL corrispondente all’espressione regolare ^/admin richieda ROLE_ADMIN:

  • YAML
    # app/config/security.yml
    security:
        # ...
        firewalls:
            # ...
    
        access_control:
            # require ROLE_ADMIN for /admin*
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
    
            <firewall name="default">
                <!-- ... -->
            </firewall>
    
            <access-control>
                <!-- require ROLE_ADMIN for /admin* -->
                <rule path="^/admin" role="ROLE_ADMIN" />
            </access-control>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'firewalls' => array(
            // ...
            'default' => array(
                // ...
            ),
        ),
       'access_control' => array(
           // require ROLE_ADMIN for /admin*
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

Questo va benissimo per proteggere intere sezioni, ma si potrebbero anche voler proteggere singoli controllori.

Si possono definire quanti schemi di URL si vuole, ciascuno con un’espressione regolare. Tuttavia, solo uno di questi avrà una corrispondenza. Symfony inizierà cercando dalla cima e si fermerà non appena troverà una voce di access_control che corrisponda all’URL.

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <!-- ... -->
            <access-control>
                <rule path="^/admin/users" role="ROLE_SUPER_ADMIN" />
                <rule path="^/admin" role="ROLE_ADMIN" />
            </access-control>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'access_control' => array(
            array('path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'),
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

Aggiungendo un ^ iniziale, solo gli URL che iniziano con lo schema corrisponderanno. Per esempio, un percorso /admin (senza ^) corrisponderebbe ad /admin/pippo, ma anche a URL come /pippo/admin.

Proteggere controllori e altro codice

Si può negare accesso da dentro un controllore:

// ...

public function helloAction($name)
{
    // Il secondo parametro si usa per specificare l'oggetto su cui si testa il ruolo
    $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Non si può accedere a questa pagina!');

    // Vecchio modo :
    // if (false === $this->get('security.authorization_checker')->isGranted('ROLE_ADMIN')) {
    //     throw $this->createAccessDeniedException('Non si può accedere a questa pagina!');
    // }

    // ...
}

Nuovo nella versione 2.6: Il metodo denyAccessUnlessGranted() è stato introdotto in Symfony 2.6. In precedenza (ma anche ora), si poteva verificare l’accesso direttamente e sollevare AccessDeniedException, come mostrato nell’esempio preedente).

Nuovo nella versione 2.6: Il servizio security.authorization_checker è stato introdotto in Symfony 2.6. Prima di Symfony 2.6, si doveva usare il metodo isGranted() del servizio security.context.

Il metodo createAccessDeniedException() crea uno speciale oggetto Symfony\Component\Security\Core\Exception\AccessDeniedException, che alla fine lancia una risposta HTTP 403 in Symfony.

Ecco fatto! Se l’utente non è ancora loggato, gli sarà richiesto il login (p.e. rinviato alla pagina di login). Se invece è loggato, gli sarà mostrata una pagina di errore 403 (che si può personalizzare).

Grazie a SensioFrameworkExtraBundle, si può anche proteggere un controllore tramite annotazioni:

// ...
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Security("has_role('ROLE_ADMIN')")
 */
public function helloAction($name)
{
    // ...
}

Per maggiori informazioni, vedere la documentazione di FrameworkExtraBundle.

Controllo degli accessi nei template

Se si vuole verificare in un template che l’utente corrente abbia un ruolo, usare la funzione aiutante predefinita:

  • Twig
    {% if is_granted('ROLE_ADMIN') %}
        <a href="...">Elimina</a>
    {% endif %}
    
  • PHP
    <?php if ($view['security']->isGranted('ROLE_ADMIN')): ?>
        <a href="...">Elimina</a>
    <?php endif ?>
    

Se si usa questa funzione non essendo dietro a un firewall, sarà lanciata un’ecceezione. È quindi sempre una buona idea avere almeno un firewall principale, che copra tutti gli URL (come mostrato in questo capitolo).

Attenzione

Prestare attenzione nel layout e nelle pagine di errore! A causa di alcuni dettagli interno di Symfony, per evitare di rompere le pagine di errore in ambiente prod, verificare prima se sia definito app.user:

{% if app.user and is_granted('ROLE_ADMIN') %}

Proteggere altri servizi

In Symfony, ogni cosa può essere protetta facendo qualcosa di simile a questo. Per esempio, si supponga di avere un servizio (cioè una classe PHP), il cui compito è inviare email. Si può restringere l’uso di questa classe, non importa dove venga usata, solo ad alcuni utenti.

Per maggiori informazioni, vedere Proteggere servizi e metodi di un’applicazione.

Verificare se un utente sia connesso (IS_AUTHENTICATED_FULLY)

Finora, i controlli sugli accessi sono stati basati su ruoli, stringhe che iniziano con ROLE_ e assegnate agli utenti. Se invece si vuole solo verificare se un utente sia connesso (senza curarsi dei ruoli), si può usare IS_AUTHENTICATED_FULLY:

// ...

public function helloAction($name)
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }

    // ...
}

Suggerimento

Si può usare questo metodo anche in access_control.

IS_AUTHENTICATED_FULLY non è un ruolo, ma si comporta come tale ed è assegnato a ciascun utente che sia sia connesso. In effetti, ci sono tre attributi speciali di questo tipo:

  • IS_AUTHENTICATED_REMEMBERED: Assegnato a tutti gli utenti connessi, anche se si sono connessi tramite un cookie “ricordami”. Anche se non si usa la funzionalità “ricordami”, lo si può usare per verificare se l’utente sia connesso.
  • IS_AUTHENTICATED_FULLY: Simile a IS_AUTHENTICATED_REMEMBERED, ma più forte. Gli utenti connessi tramite un cookie “ricordami” avranno IS_AUTHENTICATED_REMEMBERED, ma non IS_AUTHENTICATED_FULLY.
  • IS_AUTHENTICATED_ANONYMOUSLY: Assegnato a tutti gli utenti (anche quelli anonimi). Utile per mettere URL in una lista bianca per garantire accesso, alcuni dettagli sono in Come funziona access_control?.

Si possono anche usare espressioni nei template:

  • Twig
    {% if is_granted(expression(
        '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
    )) %}
        <a href="...">Delete</a>
    {% endif %}
    
  • PHP
    <?php if ($view['security']->isGranted(new Expression(
        '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
    ))): ?>
        <a href="...">Delete</a>
    <?php endif; ?>
    

Per maggiori dettagli su espressioni e sicurezza, vedere Sicurezza: controlli complessi di accesso con espressioni.

Access Control List (ACL): proteggere singoli oggetti della base dati

Si immagini di progettare un blog in cui gli utenti possono commentare i post. Si vuole anche che un utente sia in grado di modificare i propri commenti, ma non quelli di altri utenti. Inoltre, come utente amministratore, si vuole essere in grado di modificare tutti i commenti.

Per la realizzazione, si hanno due opzioni:

  • I votanti consentono di usare logica di business (p.e. l’utente può modificare i suoi commenti perché ne è il creatore) per stabilire l’accesso. Probabilmente si userà questa opzione, è abbastanza flessibile per risolvere la situazione.
  • Le ACL consentono di creare una struttura di base dati in cui si può assegnare qualsiasi accesso (p.e. EDIT, VIEW) a quasliasi utente su qualsiasi oggetto del sistema. Usarla se si ha bisogno che l’utente amministratore possa garantire accessi personalizzati nel sistema, tramite una qualche interfaccia di amministrazione.

In entrambi i casi, occorre comunque negare l’accesso usando metodi simili a quelli visti in precedenza.

Recuperare l’oggetto utente

Nuovo nella versione 2.6: Il servizio security.token_storage è stato introdotti in Symfony 2.6. Prima di Symfony 2.6, si doveva usare il metodo getToken() del servizio security.context.

Dopo l’autenticazione, si può accedere all’oggetto User dell’uente corrente tramite il servizio security.context. Da dentro un controllore, Sarà una cosa simile:

public function indexAction()
{
    if (!$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
        throw $this->createAccessDeniedException();
    }

    $user = $this->getUser();

    // il precedente è una scorciatoia per questo
    $user = $this->get('security.token_storage')->getToken()->getUser();
}

Suggerimento

L’oggetto e la classe dell’utente dipenderanno dal proprio fornitore di utenti.

Ora si possono chiamare i metodi desiderati sul proprio oggetto utente. Per esempio, se il proprio oggetto utente ha un metodo getFirstName(), lo si può usare:

use Symfony\Component\HttpFoundation\Response;

public function indexAction()
{
    // ...

    return new Response('Ciao '.$user->getFirstName());
}

Verificare sempre se l’utente è connesso

È importante verificare prima se l’utente sia autenticato. Se non lo è, $user sarà null oppure la stringa anon.. Come? Esatto, c’è una stranezza. Se non si è leggati, l’utente è tecnicamente la stringa anon., anche se la scorciatoia getUser() del controllore la converte in null per convenienza.

Il punto è questo: verificare sempre se l’utente sia connesso, prima di usare l’oggetto User e usare il metodo isGranted (o access_control) per farlo:

// Usare questo per vedere se l'utente sia connesso
if (!$this->get('security.context')->isGranted('IS_AUTHENTICATED_FULLY')) {
    throw new AccessDeniedException();
}

// Non verificare mai l'oggetto User per verdere se l'utente sia connesso
if ($this->getUser()) {

}

Recuperare l’utente in un template

In un template Twig, si può accedere all’oggetto tramite :ref:`app.user <reference-twig-global-app>`_:

  • Twig
    {% if is_granted('IS_AUTHENTICATED_FULLY') %}
        <p>Nome utente: {{ app.user.username }}</p>
    {% endif %}
    
  • PHP
    <?php if ($view['security']->isGranted('IS_AUTHENTICATED_FULLY')): ?>
        <p>Nome utente: <?php echo $app->getUser()->getUsername() ?></p>
    <?php endif; ?>
    

Logout

Solitamente, si desidera che gli utenti possano eseguire un logout. Per fortuna, il firewall può gestirlo automaticamente, se si attiva il parametro logout nella configurazione:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                logout:
                    path:   /logout
                    target: /
        # ...
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall name="secured_area" pattern="^/">
                <!-- ... -->
                <logout path="/logout" target="/" />
            </firewall>
            <!-- ... -->
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                // ...
                'logout' => array('path' => 'logout', 'target' => '/'),
            ),
        ),
        // ...
    ));
    

Quindi, si deve creare una rotta per tale URL (non serve invece un controllore):

  • YAML
    # app/config/routing.yml
    logout:
        path:   /logout
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing
            http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="logout" path="/logout" />
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('logout', new Route('/logout', array()));
    
    return $collection;
    

Ecco fatto! Se l’utente va su /logout (o sull’URL configurato in path), Symfony disconnetterà l’utente corrente.

Una volta eseguito il logout, l’utente sarà rinviato al percorso definito nel parametro target (p.e. su homepage).

Suggerimento

Se si ha bisogno di fare qualcosa d’altro dopo il logout, si può specificare un gestore di logout, aggiungendo la voce success_handler e puntandola a un servizio, che implementi Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface. Vedere Security Configuration Reference.

Codifica dinamica di una password

Se, per esempio, gli utenti sono memorizzati in una base dati, occorrerà codificare le loro password, prima di inserirle. Non importa quale algoritmo sia configurato per l’oggetto utente, l’hash della password può sempre essere determinato nel modo seguente, in un controllore:

// qualunque sia il *proprio* oggetto User
$user = new AppBundle\Entity\User();
$plainPassword = 'ryanpass';
$encoder = $this->container->get('security.password_encoder');
$encoded = $encoder->encodePassword($user, $plainPassword);

$user->setPassword($encoded);

Nuovo nella versione 2.6: Il servizio security.password_encoder è stato introdotto in Symfony 2.6.

Per poter funzionare, assicurarsi di avere un codificatore per la classe utente (p.e. AppBundle\Entity\User) configurato sotto la voce encoders in app/config/security.yml.

L’oggetto $encoder ha anche un metodo isPasswordValid, che accetta l’oggetto User come primo parametro e la password in chiaro, da verificare, come secondo parametro.

Attenzione

Quando si consente a un utente di inviare una password in chiaro (p.e. un form di registrazione, un form di cambio password), si deve avere una validazione che garantisca una lunghezza massima della password di 4096 caratteri. Maggiori dettagli su implementare una semplice form di registrazione.

Gerarchia dei ruoli

Invece di associare molti ruoli agli utenti, si possono definire regole di ereditarietà dei ruoli, creando una gerarchia:

  • YAML
    # app/config/security.yml
    security:
        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <role id="ROLE_ADMIN">ROLE_USER</role>
            <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'role_hierarchy' => array(
            'ROLE_ADMIN'       => 'ROLE_USER',
            'ROLE_SUPER_ADMIN' => array(
                'ROLE_ADMIN',
                'ROLE_ALLOWED_TO_SWITCH',
            ),
        ),
    ));
    

In questa configurazione, gli utenti con il ruolo ROLE_ADMIN avranno anche il ruolo ROLE_USER. Il ruolo ROLE_SUPER_ADMIN ha ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH e ROLE_USER (ereditato da ROLE_ADMIN).

Autenticazione senza stato

Symfony si appoggia a un cookie (la sessione) per persistere il contesto di sicurezza dell’utente. Se però si usano certificati o autenticazione HTTP, per esempio, non serve persistenza, perché le credenziali sono disponibili a ogni richiesta. In tal caso, e non si ha bisogno di memorizzare altro tra una richiesta e l’altro, si può attivare l’autenticazione senza stato (che vuol dire che Symfony non creerà alcun cookie):

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                http_basic: ~
                stateless:  true
    
  • XML
    <!-- app/config/security.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall stateless="true">
                <http-basic />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array('http_basic' => array(), 'stateless' => true),
        ),
    ));
    

Nota

Se si usa un form di login, Symfony creerà un cookie anche se si imposta stateless a true.

Verificare vulnerabilità note nelle dipendenze

Nuovo nella versione 2.5: Il comando security:check è stato introdotto in Symfony 2.5. Questo comando è incluso in SensioDistributionBundle, bundle che va registrato nell’applicazione per consentire l’utilizzo del comando stesso.

Quando si hanno molte dipendenze in progetti Symfony, alcune potrebbero contenere delle vulnerabilità. Per questo motivo, Symfony include un comando chiamato security:check, che verifica il file composer.lock e trova eventuali vulnerabilità nelle dipendenze installate:

$ php app/console security:check

Una buona pratica di sicurezza consiste nell’eseguire regolarmente questo comando, per poter aggiornare o sostituire dipendenze compromesse il prima possibile. Internamente, questo comando usa la base dati degli avvisi di sicurezza pubblicato dall’organizzazione FriendsOfPHP.

Suggerimento

Il comando security:check esce con un codice diverso da zero, se alcune dipendenze sono afflitte da problemi noti di sicurezza. Quindi, si può facilmente integrare in un processo di build.

Considerazioni finali

Ora sappiamo più di qualche base sulla sicurezza. Le parti più difficili coinvolgono i requisiti personalizzati: una strategia di autenticazione personalizzata (p.e. token API), logica di autorizzazione complessa e molte altre cose (perché la sicurezza è complessa!).

Fortunatamente, ci sono molte ricette sulla sicurezza, che descrivono molte di queste situazioni. Inoltre, vedere la sezione di riferimento della sicurezza. Molte opzioni non hanno dettagli specifici, ma analizzare l’intero albero di configurazione potrebbe essere utile.

Buona fortuna!