Creative Commons License
This work is licensed under a
Creative Commons
Attribution-Share Alike 3.0
Unported License.

Master Symfony2 fundamentals

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Symfony hosting done right

ServerGrove, outstanding support at the right price for your Symfony hosting needs.
servergrove.com

Discover the SensioLabs Support

Access to the SensioLabs Competency Center for an exclusive and tailor-made support on Symfony
sensiolabs.com

Sicurezza

La sicurezza è una procedura che avviene in due fasi, il cui obiettivo è quello di impedire a un utente di accedere a una risorsa a cui non dovrebbe avere accesso.

Nella prima fase del processo, il sistema di sicurezza identifica chi è l'utente, chiedendogli di presentare una sorta di identificazione. Quest'ultima è chiamata autenticazione e significa che il sistema sta cercando di scoprire chi sei.

Una volta che il sistema sa chi sei, il passo successivo è quello di determinare se dovresti avere accesso a una determinata risorsa. Questa parte del processo è chiamato autorizzazione e significa che il sistema verifica se disponi dei privilegi per eseguire una certa azione.

../_images/security_authentication_authorization.png

Il modo migliore per imparare è quello di vedere un esempio, iniziamo proteggendo l'applicazione con l'autenticazione base HTTP.

Note

Il componente della sicurezza di Symfony è disponibile come libreria PHP a sé stante, per l'utilizzo all'interno di qualsiasi progetto PHP.

Esempio di base: l'autenticazione HTTP

Il componente della sicurezza può essere configurato attraverso la configurazione dell'applicazione. In realtà, per molte configurazioni standard di sicurezza basta solo usare la giusta configurazione. La seguente configurazione dice a Symfony di proteggere qualunque URL corrispondente a /admin/* e chiedere le credenziali all'utente utilizzando l'autenticazione base HTTP (cioè il classico vecchio box nome utente/password):

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                pattern:    ^/
                anonymous: ~
                http_basic:
                    realm: "Area demo protetta"
    
        access_control:
            - { path: ^/admin, roles: ROLE_ADMIN }
    
        providers:
            in_memory:
                memory:
                    users:
                        ryan:  { password: ryanpass, roles: 'ROLE_USER' }
                        admin: { password: kitten, roles: 'ROLE_ADMIN' }
    
        encoders:
            Symfony\Component\Security\Core\User\User: plaintext
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <?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">
    
        <!-- app/config/security.xml -->
    
        <config>
            <firewall name="secured_area" pattern="^/">
                <anonymous />
                <http-basic realm="Area demo protetta" />
            </firewall>
    
            <access-control>
                <rule path="^/admin" role="ROLE_ADMIN" />
            </access-control>
    
            <provider name="in_memory">
                <memory>
                    <user name="ryan" password="ryanpass" roles="ROLE_USER" />
                    <user name="admin" password="kitten" roles="ROLE_ADMIN" />
                </memory>
            </provider>
    
            <encoder class="Symfony\Component\Security\Core\User\User" algorithm="plaintext" />
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                'pattern' => '^/',
                'anonymous' => array(),
                'http_basic' => array(
                    'realm' => 'Area demo protetta',
                ),
            ),
        ),
        'access_control' => array(
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array('password' => 'ryanpass', 'roles' => 'ROLE_USER'),
                        'admin' => array('password' => 'kitten', 'roles' => 'ROLE_ADMIN'),
                    ),
                ),
            ),
        ),
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => 'plaintext',
        ),
    ));
    

Tip

Una distribuzione standard di Symfony pone la configurazione di sicurezza in un file separato (ad esempio app/config/security.yml). Se non si ha un file di sicurezza separato, è possibile inserire la configurazione direttamente nel file di configurazione principale (ad esempio app/config/config.yml).

Il risultato finale di questa configurazione è un sistema di sicurezza pienamente funzionale, simile al seguente:

  • Ci sono due utenti nel sistema (ryan e admin);
  • Gli utenti si autenticano tramite autenticazione HTTP;
  • Qualsiasi URL corrispondente a /admin/* è protetto e solo l'utente admin può accedervi;
  • Tutti gli URL che non corrispondono ad /admin/* sono accessibili da tutti gli utenti (e all'utente non viene chiesto il login).

Di seguito si vedrà brevemente come funziona la sicurezza e come ogni parte della configurazione entra in gioco.

Come funziona la sicurezza: autenticazione e autorizzazione

Il sistema di sicurezza di Symfony funziona determinando l'identità di un utente (autenticazione) e poi controllando se l'utente deve avere accesso a una risorsa specifica o URL.

Firewall (autenticazione)

Quando un utente effettua una richiesta a un URL che è protetto da un firewall, viene attivato il sistema di sicurezza. Il compito del firewall è quello di determinare se l'utente deve o non deve essere autenticato e se deve autenticarsi, rimandare una risposta all'utente, avviando il processo di autenticazione.

Un firewall viene attivato quando l'URL di una richiesta in arrivo corrisponde al valore pattern dell'espressione regolare del firewall configurato. In questo esempio, pattern (^/) corrisponderà a ogni richiesta in arrivo. Il fatto che il firewall venga attivato non significa tuttavia che venga visualizzato il box di autenticazione con nome utente e password per ogni URL. Per esempio, qualunque utente può accedere a /foo senza che venga richiesto di autenticarsi.

../_images/security_anonymous_user_access.png

Questo funziona in primo luogo perché il firewall consente utenti anonimi, attraverso il parametro di configurazione anonymous. In altre parole, il firewall non richiede all'utente di fare immediatamente un'autenticazione. E poiché non è necessario nessun ruolo speciale per accedere a /foo (sotto la sezione access_control), la richiesta può essere soddisfatta senza mai chiedere all'utente di autenticarsi.

Se si rimuove la chiave anonymous, il firewall chiederà sempre l'autenticazione all'utente.

Controlli sull'accesso (autorizzazione)

Se un utente richiede /admin/foo, il processo ha un diverso comportamento. Questo perché la sezione di configurazione access_control dice che qualsiasi URL che corrispondono allo schema dell'espressione regolare ^/admin (cioè /admin o qualunque URL del tipo /admin/*) richiede il ruolo ROLE_ADMIN. I ruoli sono la base per la maggior parte delle autorizzazioni: un utente può accedere /admin/foo solo se ha il ruolo ROLE_ADMIN.

../_images/security_anonymous_user_denied_authorization.png

Come prima, quando l'utente effettua inizialmente la richiesta, il firewall non chiede nessuna identificazione. Tuttavia, non appena il livello di controllo di accesso nega l'accesso all'utente (perché l'utente anonimo non ha il ruolo ROLE_ADMIN), il firewall entra in azione e avvia il processo di autenticazione. Il processo di autenticazione dipende dal meccanismo di autenticazione in uso. Per esempio, se si sta utilizzando il metodo di autenticazione tramite form di login, l'utente verrà rinviato alla pagina di login. Se si utilizza l'autenticazione HTTP, all'utente sarà inviata una risposta HTTP 401 e verrà visualizzato una finestra del browser con nome utente e password.

Ora l'utente ha la possibilità di inviare le credenziali all'applicazione. Se le credenziali sono valide, può essere riprovata la richiesta originale.

../_images/security_ryan_no_role_admin_access.png

In questo esempio, l'utente ryan viene autenticato con successo con il firewall. Ma poiché ryan non ha il ruolo ROLE_ADMIN, viene ancora negato l'accesso a /admin/foo. In definitiva, questo significa che l'utente vedrà un qualche messaggio che indica che l'accesso è stato negato.

Tip

Quando Symfony nega l'accesso all'utente, l'utente vedrà una schermata di errore e riceverà un codice di stato HTTP 403 (Forbidden). È possibile personalizzare la schermata di errore di accesso negato seguendo le istruzioni sulle pagine di errore presenti nel ricettario per personalizzare la pagina di errore 403.

Infine, se l'utente admin richiede /admin/foo, avviene un processo simile, solo che adesso, dopo essere stato autenticato, il livello di controllo di accesso lascerà passare la richiesta:

../_images/security_admin_role_access.png

Il flusso di richiesta quando un utente richiede una risorsa protetta è semplice, ma incredibilmente flessibile. Come si vedrà in seguito, l'autenticazione può essere gestita in molti modi, come un form di login, un certificato X.509, o da un'autenticazione dell'utente tramite Twitter. Indipendentemente dal metodo di autenticazione, il flusso di richiesta è sempre lo stesso:

  1. Un utente accede a una risorsa protetta;
  2. L'applicazione rinvia l'utente al form di login;
  3. L'utente invia le proprie credenziali (ad esempio nome utente / password);
  4. Il firewall autentica l'utente;
  5. L'utente autenticato riprova la richiesta originale.

Note

L'esatto processo in realtà dipende un po' da quale meccanismo di autenticazione si sta usando. Per esempio, quando si utilizza il form di login, l'utente invia le sue credenziali a un URL che elabora il form (ad esempio /login_check) e poi viene rinviato all'URL originariamente richiesto (ad esempio /admin/foo). Ma con l'autenticazione HTTP, l'utente invia le proprie credenziali direttamente all'URL originale (ad esempio /admin/foo) e poi la pagina viene restituita all'utente nella stessa richiesta (cioè senza rinvio).

Questo tipo di idiosincrasie non dovrebbe causare alcun problema, ma è bene tenerle a mente.

Tip

Più avanti si imparerà che in Symfony2 qualunque cosa può essere protetta, tra cui controllori specifici, oggetti o anche metodi PHP.

Utilizzo di un form di login tradizionale

Tip

In questa sezione, si imparerà come creare un form di login di base, che continua a usare gli utenti inseriti manualmente nel file security.yml.

Per caricare utenti da una base dati, si legga Come caricare gli utenti dalla base dati (il fornitore di entità). Leggendo quell'articolo e questa sezione, si può creare un form di login completo, che carichi utenti da una base dati.

Finora, si è visto come proteggere l'applicazione con un firewall e poi proteggere l'accesso a determinate aree tramite i ruoli. Utilizzando l'autenticazione HTTP, si può sfruttare senza fatica il box nativo nome utente/password offerto da tutti i browser. Tuttavia, Symfony supporta nativamente molti meccanismi di autenticazione. Per i dettagli su ciascuno di essi, vedere il Riferimento sulla configurazione di sicurezza.

In questa sezione, si potrà proseguire l'apprendimento, consentendo all'utente di autenticarsi attraverso un tradizionale form di login HTML.

In primo luogo, abilitare il form di login sotto il firewall:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                pattern:    ^/
                anonymous: ~
                form_login:
                    login_path:  login
                    check_path:  login_check
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <?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">
    
        <!-- app/config/security.xml -->
    
        <config>
            <firewall name="secured_area" pattern="^/">
                <anonymous />
                <form-login login_path="login" check_path="login_check" />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                'pattern' => '^/',
                'anonymous' => array(),
                'form_login' => array(
                    'login_path' => 'login',
                    'check_path' => 'login_check',
                ),
            ),
        ),
    ));
    

Tip

Se non è necessario personalizzare i valori login_path o check_path (i valori usati qui sono i valori predefiniti), è possibile accorciare la configurazione:

  • YAML
    1
    form_login: ~
    
  • XML
    1
    <form-login />
    
  • PHP
    1
    'form_login' => array(),
    

Ora, quando il sistema di sicurezza inizia il processo di autenticazione, rinvierà l'utente al form di login (/login per impostazione predefinita). Implementare visivamente il form di login è compito dello sviluppatore. In primo luogo, bisogna creare le due rotte usate nella configurazione della sicurezza: : la rotta login, che visualizzerà il form di login (cioè /login) e la rotta login_check, che gestirà l'invio del form di login (cioè /login_check):

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/routing.yml
    login:
        pattern:   /login
        defaults:  { _controller: AcmeSecurityBundle:Security:login }
    login_check:
        pattern:   /login_check
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- 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="login" pattern="/login">
            <default key="_controller">AcmeSecurityBundle:Security:login</default>
        </route>
        <route id="login_check" pattern="/login_check" />
    
    </routes>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('login', new Route('/login', array(
        '_controller' => 'AcmeDemoBundle:Security:login',
    )));
    $collection->add('login_check', new Route('/login_check', array()));
    
    return $collection;
    

Note

Non è necessario implementare un controllore per l'URL /login_check perché il firewall catturerà ed elaborerà qualunque form inviato a questo URL.

New in version 2.1: A partire da Symfony 2.1, si devono avere rotte configurate per i propri URL login_path (p.e. /login), check_path (p.e. /login_check) e logout (p.e. /logout, vedere Logout).

Notare che il nome della rotta login corrisponde al valore di configurazione login_path, in quanto è lì che il sistema di sicurezza rinvierà gli utenti che necessitano di effettuare il login.

Successivamente, creare il controllore che visualizzerà il form di login:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/Acme/SecurityBundle/Controller/SecurityController.php;
namespace Acme\SecurityBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class SecurityController extends Controller
{
    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();

        // verifica di eventuali errori
        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(
            'AcmeSecurityBundle:Security:login.html.twig',
            array(
                // ultimo nome utente inserito
                'last_username' => $session->get(SecurityContext::LAST_USERNAME),
                'error'         => $error,
            )
        );
    }
}

Non bisogna farsi confondere da questo controllore. Come si vedrà a momenti, quando l'utente compila il form, il sistema di sicurezza lo gestisce automaticamente. Se l'utente ha inviato un nome utente o una password non validi, questo controllore legge l'errore di invio del form dal sistema di sicurezza, in modo che possano essere visualizzati all'utente.

In altre parole, il vostro compito è quello di visualizzare il form di login e gli eventuali errori di login che potrebbero essersi verificati, ma è il sistema di sicurezza stesso che si prende cura di verificare il nome utente e la password inviati e di autenticare l'utente.

Infine, creare il template corrispondente:

  • Twig
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {# src/Acme/SecurityBundle/Resources/views/Security/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" />
    
        {#
            Se si desidera controllare l'URL a cui l'utente viene rinviato in caso di successo (maggiori dettagli qui sotto)
            <input type="hidden" name="_target_path" value="/account" />
        #}
    
        <button type="submit">login</button>
    </form>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?php // src/Acme/SecurityBundle/Resources/views/Security/login.html.php ?>
    <?php if ($error): ?>
        <div><?php echo $error->getMessage() ?></div>
    <?php endif; ?>
    
    <form action="<?php echo $view['router']->generate('login_check') ?>" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="<?php echo $last_username ?>" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        <!--
            Se si desidera controllare l'URL a cui l'utente viene rinviato in caso di successo (maggiori dettagli qui sotto)
            <input type="hidden" name="_target_path" value="/account" />
        -->
    
        <button type="submit">login</button>
    </form>
    

Tip

La variabile error passata nel template è un'istanza di AuthenticationException. Potrebbe contenere informazioni, anche sensibili, sull'errore di autenticazione: va quindi usata con cautela.

Il form ha pochi requisiti. In primo luogo, inviando il form a /login_check (tramite la rotta login_check), il sistema di sicurezza intercetterà l'invio del form e lo processerà automaticamente. In secondo luogo, il sistema di sicurezza si aspetta che i campi inviati siano chiamati _username e _password (questi nomi di campi possono essere configurati).

E questo è tutto! Quando si invia il form, il sistema di sicurezza controllerà automaticamente le credenziali dell'utente e autenticherà l'utente o rimanderà l'utente al form di login, dove sono visualizzati gli errori.

Rivediamo l'intero processo:

  1. L'utente prova ad accedere a una risorsa protetta;
  2. Il firewall avvia il processo di autenticazione rinviando l'utente al form di login (/login);
  3. La pagina /login rende il form di login, attraverso la rotta e il controllore creato in questo esempio;
  4. L'utente invia il form di login /login_check;
  5. Il sistema di sicurezza intercetta la richiesta, verifica le credenziali inviate dall'utente, autentica l'utente se sono corrette e, se non lo sono, lo rinvia al form di login.

Per impostazione predefinita, se le credenziali inviate sono corrette, l'utente verrà rinviato alla pagina originale che è stata richiesta (ad esempio /admin/foo). Se l'utente originariamente è andato direttamente alla pagina di login, sarà rinviato alla pagina iniziale. Questo comportamento può essere personalizzato, consentendo, ad esempio, di rinviare l'utente a un URL specifico.

Per maggiori dettagli su questo e su come personalizzare in generale il processo di login con il form, vedere Come personalizzare il form di login.

Quando si imposta il proprio form di login, bisogna fare attenzione a non incorrere in alcuni errori comuni.

1. Creare le rotte giuste

In primo luogo, essere sicuri di aver definito correttamente le rotte /login e /login_check e che corrispondano ai valori di configurazione login_path e check_path. Un errore di configurazione qui può significare che si viene rinviati a una pagina 404 invece che nella pagina di login, o che inviando il form di login non succede nulla (continuando a vedere sempre il form di login).

2. Assicurarsi che la pagina di login non sia protetta

Inoltre, bisogna assicurarsi che la pagina di login non richieda nessun ruolo per essere visualizzata. Per esempio, la seguente configurazione, che richiede il ruolo ROLE_ADMIN per tutti gli URL (includendo l'URL /login), causerà un loop di redirect:

  • YAML
    1
    2
    access_control:
        - { path: ^/, roles: ROLE_ADMIN }
    
  • XML
    1
    2
    3
    <access-control>
        <rule path="^/" role="ROLE_ADMIN" />
    </access-control>
    
  • PHP
    1
    2
    3
    'access_control' => array(
        array('path' => '^/', 'role' => 'ROLE_ADMIN'),
    ),
    

Rimuovendo il controllo degli accessi sull'URL /login il problema si risolve:

  • YAML
    1
    2
    3
    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: ROLE_ADMIN }
    
  • XML
    1
    2
    3
    4
    <access-control>
        <rule path="^/login" role="IS_AUTHENTICATED_ANONYMOUSLY" />
        <rule path="^/" role="ROLE_ADMIN" />
    </access-control>
    
  • PHP
    1
    2
    3
    4
    'access_control' => array(
        array('path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'),
        array('path' => '^/', 'role' => 'ROLE_ADMIN'),
    ),
    

Inoltre, se il firewall non consente utenti anonimi, sarà necessario creare un firewall speciale, che consenta agli utenti anonimi la pagina di login:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    firewalls:
        login_firewall:
            pattern:    ^/login$
            anonymous:  ~
        secured_area:
            pattern:    ^/
            form_login: ~
    
  • XML
    1
    2
    3
    4
    5
    6
    <firewall name="login_firewall" pattern="^/login$">
        <anonymous />
    </firewall>
    <firewall name="secured_area" pattern="^/">
        <form_login />
    </firewall>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    'firewalls' => array(
        'login_firewall' => array(
            'pattern' => '^/login$',
            'anonymous' => array(),
        ),
        'secured_area' => array(
            'pattern' => '^/',
            'form_login' => array(),
        ),
    ),
    

3. Assicurarsi che ``/login_check`` sia dietro al firewall

Quindi, assicurarsi che l'URL check_path (ad esempio /login_check) sia dietro al firewall che si sta usando per il form di login (in questo esempio, l'unico firewall fa passare tutti gli URL, includendo /login_check). Se /login_check non corrisponde a nessun firewall, si riceverà un'eccezione Unable to find the controller for path "/login_check".

4. Più firewall non condividono il contesto di sicurezza

Se si utilizzano più firewall e ci si autentica su un firewall, non si verrà autenticati automaticamente su qualsiasi altro firewall. Firewall diversi sono come diversi sistemi di sicurezza. Ecco perché occorre definire esplicitamente lo stesso Contesto del firewall per firewall diversi. Ma, per la maggior parte delle applicazioni, un solo firewall è sufficiente.

Autorizzazione

Il primo passo per la sicurezza è sempre l'autenticazione: il processo di verificare l'identità dell'utente. Con Symfony, l'autenticazione può essere fatta in qualunque modo, attraverso un form di login, autenticazione HTTP o anche tramite Facebook.

Una volta che l'utente è stato autenticato, l'autorizzazione ha inizio. L'autorizzazione fornisce un metodo standard e potente per decidere se un utente può accedere a una qualche risorsa (un URL, un oggetto del modello, una chiamata a metodo, ...). Questo funziona tramite l'assegnazione di specifici ruoli a ciascun utente e quindi richiedendo ruoli diversi per differenti risorse.

Il processo di autorizzazione ha due diversi lati:

  1. L'utente ha un insieme specifico di ruoli;
  2. Una risorsa richiede un ruolo specifico per poter accedervi.

In questa sezione, ci si concentrerà su come proteggere risorse diverse (ad esempio gli URL, le chiamate a metodi, ecc) con ruoli diversi. Più avanti, si imparerà di più su come i ruoli sono creati e assegnati agli utenti.

Protezione di specifici schemi di URL

Il modo più semplice per proteggere parte dell'applicazione è quello di proteggere un intero schema di URL. Si è già visto questo nel primo esempio di questo capitolo, dove tutto ciò a cui corrisponde lo schema di espressione regolare ^/admin richiede il ruolo ROLE_ADMIN.

Caution

La piena comprensione del funzionamento di access_control è molto importante per far sì che la propria applicaizone sia veramente sicura. Si veda Capire come funziona access_control più avanti per informazioni dettagliate.

È possibile definire tanti schemi di URL quanti ne occorrono, ciascuno è un'espressione regolare.

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    1
    2
    3
    4
    5
    6
    <!-- app/config/security.xml -->
    <config>
        <!-- ... -->
        <rule path="^/admin/users" role="ROLE_SUPER_ADMIN" />
        <rule path="^/admin" role="ROLE_ADMIN" />
    </config>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    // 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'),
        ),
    ));
    

Tip

Anteporre il percorso con il simbolo ^ assicura che corrispondano solo gli URL che iniziano con lo schema. Per esempio, un semplice percorso /admin (senza simbolo ^) corrisponderebbe correttamente a /admin/foo, ma corrisponderebbe anche a URL come /foo/admin.

Capire come funziona access_control

Per ogni richiesta in arrivo, Symfony2 verifica ogni voce di access_control per trovarne una che corrisponda alla richiesta attuale. Se ne trova una corrispondente, si ferma, quindi solo la prima voce di access_control corrispondente verrà usata per garantire l'accesso.

Ogni access_control ha varie opzioni che configurano varie cose: (a) se la richiesta in arrivo deve corrispondere a questa voce di controllo di accesso e (b) una volta corrisposta, se alcune restrizioni di accesso debbano essere applicate:

(a) Opzioni di corrispondenza

Symfony2 crea un'istanza di RequestMatcher per ogni voce di access_control, che determina se un dato controllo di accesso vada usato o meno su questa richiesta. Le seguenti opzioni di access_control sono usate per le corrispondenze:

  • path
  • ip
  • host
  • methods

Si prende il seguente access_control come esempio:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
            - { path: ^/admin, roles: ROLE_USER_HOST, host: symfony.com }
            - { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
            - { path: ^/admin, roles: ROLE_USER }
    
  • XML
    1
    2
    3
    4
    5
    6
    <access-control>
        <rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
        <rule path="^/admin" role="ROLE_USER_HOST" host="symfony.com" />
        <rule path="^/admin" role="ROLE_USER_METHOD" method="POST, PUT" />
        <rule path="^/admin" role="ROLE_USER" />
    </access-control>
    
  • PHP
    1
    2
    3
    4
    5
    6
    'access_control' => array(
        array('path' => '^/admin', 'role' => 'ROLE_USER_IP', 'ip' => '127.0.0.1'),
        array('path' => '^/admin', 'role' => 'ROLE_USER_HOST', 'host' => 'symfony.com'),
        array('path' => '^/admin', 'role' => 'ROLE_USER_METHOD', 'method' => 'POST, PUT'),
        array('path' => '^/admin', 'role' => 'ROLE_USER'),
    ),
    

Per ogni richiesta in arrivo, Symfony2 deciderà quale access_control usare in base a URI, indirizzo IP del client, nome host in arrivo, metodo della richiestsa. Si ricordi, viene usata la prima regola corrispondnete e, se ip, host o method non sono specificati per una voce, access_control corrisponderà per qualsiasi ip, host o method:

URI IP HOST METODO access_control Perché?
/admin/user 127.0.0.1 example.com GET regola #1 (ROLE_USER_IP) L'URI corrisponde a path e l'IP a ip.
/admin/user 127.0.0.1 symfony.com GET regola #1 (ROLE_USER_IP) path e ip corrispondono. Corrisponderebbe anche ROLE_USER_HOST, ma solo se si usa la prima corrispondenza di access_control.
/admin/user 168.0.0.1 symfony.com GET regola #2 (ROLE_USER_HOST) ip non corrisponde alla prima regola, quindi viene usata la seconda (che corrisponde).
/admin/user 168.0.0.1 symfony.com POST regola #2 (ROLE_USER_HOST) La seconda regola corrisponde. Corrisponderebbe anche la terza regola (ROLE_USER_METHOD), ma solo la prima corrispondenza di access_control viene usata.
/admin/user 168.0.0.1 example.com POST reg. #3 (ROLE_USER_METHOD) ip e host non corrispondono alle prime due voci, la terza, ROLE_USER_METHOD, corrisponde e viene usata.
/admin/user 168.0.0.1 example.com GET regola #4 (ROLE_USER) ip, host e method non fanno corrispondere le prime tre voci. Ma siccome l'URI corrisponde a path di ROLE_USER, viene usata.
/foo 127.0.0.1 symfony.com POST nessuna corrispondenza Non corrisponde ad alcune regola di access_control, poiché l'URI non corrisponde ad alcun valore di path.

(b) Controllo dell'accesso

Una volta che Symfony2 ha deciso quale voce di access_control corrisponda, applica restrizioni di accesso in base alle opzioni roles e requires_channel:

  • role Se l'utente non ha il ruolo fornito, l'accesso viene negato (internamente, viene lanciata AccessDeniedException);
  • requires_channel Se il canale della richiesta in arrivo (p.e. http) non corrisponde a questo valore (p.e. https), l'utente sarà rinviato (p.e. rinviato da http a https, o viceversa).

Tip

In caso di accesso negato, il sistema proverà ad autenticare l'utente, se non lo è già (p.e. rinviare l'utente alla pagina di login). Se l'utente è già entrato, verrà mostrata la pagina di errore 403 "access denied". Si veda Come personalizzare le pagine di errore per ulteriori informazioni.

Protezione tramite IP

In certe situazioni può succedere di limitare l'accesso a una data rotta basata su IP. Questo è particolarmente rilevante nel caso di Edge Side Includes (ESI), per esempio. Quando ESI è abilitato, si raccomanda di proteggere l'accesso agli URL ESI. Infatti, alcuni ESI possono contenere contenuti privati, come informazioni sull'utente attuale. Per prevenire un accesso diretto a tali risorse inserendo direttamnte l'URL nel browser, la rotta ESI deve essere protetta e resa visibile solo dalla cache del reverse proxy.

Ecco un esempio di come si possano garantire tutte le rotte ESI che iniziano per un certo prefisso, /esi, da intrusioni esterne:

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/esi, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
            - { path: ^/esi, roles: ROLE_NO_ACCESS }
    
  • XML
    1
    2
    3
    4
    <access-control>
        <rule path="^/esi" role="IS_AUTHENTICATED_ANONYMOUSLY" ip="127.0.0.1" />
        <rule path="^/esi" role="ROLE_NO_ACCESS" />
    </access-control>
    
  • PHP
    1
    2
    3
    4
    'access_control' => array(
        array('path' => '^/esi', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'ip' => '127.0.0.1'),
        array('path' => '^/esi', 'role' => 'ROLE_NO_ACCESS'),
    ),
    

Ecco come funziona quando il percorso è /esi/qualcosa dall'IP 10.0.0.1:

  • La prima regola di controllo di accesso non corrisponde e viene ignorata, perché path corrisponde, ma ip no;
  • La seconda regola di controllo di accesso non corrisponde (essendoci solo path, che corrisponde): non avendo l'utente il ruolo ROLE_NO_ACCESS, perché non definito, l'accesso è negato (il ruolo ROLE_NO_ACCESS può essere qualsiasi cosa che non sia un ruolo esistente, serve solo come espediente per negare sempre l'accesso).

Se ora la stessa richiesta arriva da 127.0.0.1:

  • Ora, la prima regola di controllo di accesso corrisponde sia per path che per ip: l'accesso è consentito, perché l'utente ha sempre il ruolo IS_AUTHENTICATED_ANONYMOUSLY.
  • La seconda regola di accesso non viene esaminata, perché la prima corrispondeva.

Protezione tramite canale

Si può anche richiedere di accedere a un URL tramite SSL, basta usare la voce aggiungere il parametro requires_channel in una voce access_control:

  • YAML
    1
    2
    3
    4
    5
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
    
  • XML
    1
    2
    3
    <access-control>
        <rule path="^/cart/checkout" role="IS_AUTHENTICATED_ANONYMOUSLY" requires_channel="https" />
    </access-control>
    
  • PHP
    1
    2
    3
    'access_control' => array(
        array('path' => '^/cart/checkout', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https'),
    ),
    

Proteggere un controllore

Proteggere l'applicazione basandosi su schemi di URL è semplice, ma in alcuni casi può non essere abbastanza granulare. Quando necessario, si può facilmente forzare l'autorizzazione dall'interno di un controllore:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

public function helloAction($name)
{
    if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }

    // ...
}

È anche possibile scegliere di installare e utilizzare l'opzionale JMSSecurityExtraBundle, che può proteggere il controllore utilizzando le annotazioni:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ...
use JMS\SecurityExtraBundle\Annotation\Secure;

/**
 * @Secure(roles="ROLE_ADMIN")
 */
public function helloAction($name)
{
    // ...
}

Per maggiori informazioni, vedere la documentazione di JMSSecurityExtraBundle. Se si sta utilizzando la distribuzione standard di Symfony, questo bundle è disponibile per impostazione predefinita. In caso contrario, si può facilmente scaricare e installare.

Protezione degli altri servizi

In realtà, con Symfony si può proteggere qualunque cosa, utilizzando una strategia simile a quella vista nella sezione precedente. Per esempio, si supponga di avere un servizio (ovvero una classe PHP) il cui compito è quello di inviare email da un utente all'altro. È possibile limitare l'uso di questa classe, non importa dove è stata utilizzata, per gli utenti che hanno un ruolo specifico.

Per ulteriori informazioni su come utilizzare il componente della sicurezza per proteggere servizi e metodi diversi nell'applicazione, vedere Proteggere servizi e metodi di un'applicazione.

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

Si immagini di progettare un sistema di blog, in cui gli utenti possono commentare i messaggi. Si vuole che un utente possa modificare i propri commenti, ma non quelli degli altri. Inoltre, come utente admin, si vuole essere in grado di modificare tutti i commenti.

Il componente della sicurezza viene fornito con un sistema opzionale di access control list (ACL), che è possibile utilizzare quando è necessario controllare l'accesso alle singole istanze di un oggetto nel sistema. Senza ACL, è possibile proteggere il sistema in modo che solo certi utenti possono modificare i commenti sui blog. Ma con ACL, si può limitare o consentire l'accesso commento per commento.

Per maggiori informazioni, vedere l'articolo del ricettario: Access Control List (ACL).

Utenti

Nelle sezioni precedenti, si è appreso come sia possibile proteggere diverse risorse, richiedendo una serie di ruoli per una risorsa. In questa sezione, esploreremo l'altro lato delle autorizzazioni: gli utenti.

Da dove provengono gli utenti? (User Provider)

Durante l'autenticazione, l'utente invia un insieme di credenziali (di solito un nome utente e una password). Il compito del sistema di autenticazione è quello di soddisfare queste credenziali con l'insieme degli utenti. Quindi da dove proviene questa lista di utenti?

In Symfony2, gli utenti possono arrivare da qualsiasi parte: un file di configurazione, una tabella di una base dati, un servizio web o qualsiasi altra cosa si può pensare. Qualsiasi cosa che prevede uno o più utenti nel sistema di autenticazione è noto come "fornitore di utenti". Symfony2 viene fornito con i due fornitori utenti più diffusi; uno che carica gli utenti da un file di configurazione e uno che carica gli utenti da una tabella di una base dati.

Definizione degli utenti in un file di configurazione

Il modo più semplice per specificare gli utenti è direttamente in un file di configurazione. In effetti, questo si è già aver visto nell'esempio di questo capitolo.

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # app/config/security.yml
    security:
        # ...
        providers:
            default_provider:
                memory:
                    users:
                        ryan:  { password: ryanpass, roles: 'ROLE_USER' }
                        admin: { password: kitten, roles: 'ROLE_ADMIN' }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    <!-- app/config/security.xml -->
    <config>
        <!-- ... -->
        <provider name="default_provider">
            <memory>
                <user name="ryan" password="ryanpass" roles="ROLE_USER" />
                <user name="admin" password="kitten" roles="ROLE_ADMIN" />
            </memory>
        </provider>
    </config>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // app/config/security.php
    $container->loadFromExtension('security', array(
        ...,
        'providers' => array(
            'default_provider' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array('password' => 'ryanpass', 'roles' => 'ROLE_USER'),
                        'admin' => array('password' => 'kitten', 'roles' => 'ROLE_ADMIN'),
                    ),
                ),
            ),
        ),
    ));
    

Questo fornitore utenti è chiamato "in-memory" , dal momento che gli utenti non sono memorizzati in una base dati. L'oggetto utente effettivo è fornito da Symfony (User).

Tip

Qualsiasi fornitore utenti può caricare gli utenti direttamente dalla configurazione, specificando il parametro di configurazione users ed elencando gli utenti sotto di esso.

Caution

Se il nome utente è completamente numerico (ad esempio 77) o contiene un trattino (ad esempio user-name), è consigliabile utilizzare la seguente sintassi alternativa quando si specificano utenti in YAML:

1
2
3
users:
    - { name: 77, password: pass, roles: 'ROLE_USER' }
    - { name: user-name, password: pass, roles: 'ROLE_USER' }

Per i siti più piccoli, questo metodo è semplice e veloce da configurare. Per sistemi più complessi, si consiglia di caricare gli utenti dalla base dati.

Caricare gli utenti da una base dati

Se si vuole caricare gli utenti tramite l'ORM Doctrine, si può farlo facilmente attraverso la creazione di una classe User e configurando il fornitore entity.

Con questo approccio, bisogna prima creare la propria classe User, che sarà memorizzata nella base dati.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

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

/**
 * @ORM\Entity
 */
class User implements UserInterface
{
    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $username;

    // ...
}

Per come è stato pensato il sistema di sicurezza, l'unico requisito per la classe utente personalizzata è che implementi l'interfaccia UserInterface. Questo significa che il concetto di "utente" può essere qualsiasi cosa, purché implementi questa interfaccia.

New in version 2.1: In Symfony 2.1, il metodo equals è stato rimosso da UserInterface. Se occorre ridefinire l'implementazione originale della logica di confronto, implementare la nuova interfaccia EquatableInterface.

Note

L'oggetto utente verrà serializzato e salvato nella sessione durante le richieste, quindi si consiglia di implementare l'interfaccia Serializable nel proprio oggetto utente. Ciò è particolarmente importante se la classe User ha una classe genitore con proprietà private.

Quindi, configurare un fornitore utenti entity e farlo puntare alla classe User:

  • YAML
    1
    2
    3
    4
    5
    # app/config/security.yml
    security:
        providers:
            main:
                entity: { class: Acme\UserBundle\Entity\User, property: username }
    
  • XML
    1
    2
    3
    4
    5
    6
    <!-- app/config/security.xml -->
    <config>
        <provider name="main">
            <entity class="Acme\UserBundle\Entity\User" property="username" />
        </provider>
    </config>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'main' => array(
                'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'),
            ),
        ),
    ));
    

Con l'introduzione di questo nuovo fornitore, il sistema di autenticazione tenterà di caricare un oggetto User dalla base dati, utilizzando il campo username di questa classe.

Note

Questo esempio ha come unico scopo quello di mostrare l'idea di base dietro al fornitore entity. Per un esempio completamente funzionante, vedere Come caricare gli utenti dalla base dati (il fornitore di entità).

Per ulteriori informazioni sulla creazione di un proprio fornitore personalizzato (ad esempio se è necessario caricare gli utenti tramite un servizio web), vedere Come creare un fornitore utenti personalizzato.

Codificare la password dell'utente

Finora, per semplicità, tutti gli esempi hanno memorizzato le password dell'utente in formato testuale (se tali utenti sono memorizzati in un file di configurazione o in una base dati). Naturalmente, in un'applicazione reale si consiglia, per ragioni di sicurezza, di codificare le password degli utenti. Questo è facilmente realizzabile mappando la classe User in uno dei numerosi "encoder" disponibili. Per esempio, per salvare gli utenti in memoria, ma oscurare le loro password tramite sha1, si può fare come segue:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    # app/config/security.yml
    security:
        # ...
        providers:
            in_memory:
                memory:
                    users:
                        ryan:  { password: bb87a29949f3a1ee0559f8a57357487151281386, roles: 'ROLE_USER' }
                        admin: { password: 74913f5cd5f61ec0bcfdb775414c2fb3d161b620, roles: 'ROLE_ADMIN' }
    
        encoders:
            Symfony\Component\Security\Core\User\User:
                algorithm:   sha1
                iterations: 1
                encode_as_base64: false
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- app/config/security.xml -->
    <config>
        <!-- ... -->
        <provider name="in_memory">
            <memory>
                <user name="ryan" password="bb87a29949f3a1ee0559f8a57357487151281386" roles="ROLE_USER" />
                <user name="admin" password="74913f5cd5f61ec0bcfdb775414c2fb3d161b620" roles="ROLE_ADMIN" />
            </memory>
        </provider>
    
        <encoder class="Symfony\Component\Security\Core\User\User" algorithm="sha1" iterations="1" encode_as_base64="false" />
    </config>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array('password' => 'bb87a29949f3a1ee0559f8a57357487151281386', 'roles' => 'ROLE_USER'),
                        'admin' => array('password' => '74913f5cd5f61ec0bcfdb775414c2fb3d161b620', 'roles' => 'ROLE_ADMIN'),
                    ),
                ),
            ),
        ),
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => array(
                'algorithm'         => 'sha1',
                'iterations'        => 1,
                'encode_as_base64'  => false,
            ),
        ),
    ));
    

Impostando iterations a 1 ed encode_as_base64 a false, viene eseguito una sola volta l'algoritmo sha1 sulla password, senza alcuna codifica supplementare. È ora possibile calcolare l'hash della password a livello di codice (ad esempio hash('sha1', 'ryanpass')) o tramite qualche strumento online come functions-online.com

Se si stanno creando i propri utenti in modo dinamico (memorizzandoli in una base dati), è possibile utilizzare algoritmi di hash ancora più complessi e poi contare su un oggetto encoder, che aiuti a codificare le password. Per esempio, supponiamo che l'oggetto User sia Acme\UserBundle\Entity\User (come nell'esempio precedente). In primo luogo, configurare l'encoder per questo utente:

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/security.yml
    security:
        # ...
    
        encoders:
            Acme\UserBundle\Entity\User: sha512
    
  • XML
    1
    2
    3
    4
    5
    6
    <!-- app/config/security.xml -->
    <config>
        <!-- ... -->
    
        <encoder class="Acme\UserBundle\Entity\User" algorithm="sha512" />
    </config>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    // app/config/security.php
    $container->loadFromExtension('security', array(
        ...,
        'encoders' => array(
            'Acme\UserBundle\Entity\User' => 'sha512',
        ),
    ));
    

In questo caso, si utilizza il più forte algoritmo sha512. Inoltre, poiché si è semplicemente specificato l'algoritmo (sha512) come stringa, il sistema per impostazione predefinita farà l'hash della password 5000 volte di seguito e poi la codificherà in base64. In altre parole, la password è stata notevolmente offuscata in modo che il suo hash non possa essere decodificato (cioè non è possibile determinare la password partendo dal suo hash).

New in version 2.2: Da Symfony 2.2, si possono usare anche i codificatori PBKDF2 e BCrypt.

Determinare la password con hash

Se si ha un form di registrazione per gli utenti, è necessario essere in grado di determinare l'hash della password, in modo che sia possibile impostarla per l'utente. Indipendentemente dall'algoritmo configurato per l'oggetto User, l'hash della password può essere determinato nel seguente modo da un controllore:

1
2
3
4
5
6
$factory = $this->get('security.encoder_factory');
$user = new Acme\UserBundle\Entity\User();

$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword('ryanpass', $user->getSalt());
$user->setPassword($password);

Recuperare l'oggetto User

Dopo l'autenticazione, si può accedere all'oggetto User per l'utente corrente tramite il servizio security.context. Da dentro un controllore, assomiglierà a questo:

1
2
3
4
public function indexAction()
{
    $user = $this->get('security.context')->getToken()->getUser();
}

In un controllore, si può usare una scorciatoia:

1
2
3
4
public function indexAction()
{
    $user = $this->getUser();
}

Note

Gli utenti anonimi sono tecnicamente autenticati, nel senso che il metodo isAuthenticated() dell'oggetto di un utente anonimo restituirà true. Per controllare se l'utente sia effettivamente autenticato, verificare il ruolo IS_AUTHENTICATED_FULLY.

In un template Twig, si può accedere a questo oggetto tramite la chiave app.user, che richiama il metodo GlobalVariables::getUser():

  • Twig
    1
    <p>Nome utente: {{ app.user.username }}</p>
    
  • PHP
    1
    <p>Nome utente: <?php echo $app->getUser()->getUsername() ?></p>
    

Utilizzare fornitori utenti multipli

Ogni meccanismo di autenticazione (ad esempio l'autenticazione HTTP, il form di login, ecc.) utilizza esattamente un fornitore utenti e, per impostazione predefinita, userà il primo fornitore dichiarato. Ma cosa succede se si desidera specificare alcuni utenti tramite configurazione e il resto degli utenti nella base dati? Questo è possibile attraverso la creazione di un nuovo fornitore, che li unisca:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    # app/config/security.yml
    security:
        providers:
            chain_provider:
                chain:
                    providers: [in_memory, user_db]
            in_memory:
                memory:
                    users:
                        foo: { password: test }
            user_db:
                entity: { class: Acme\UserBundle\Entity\User, property: username }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- app/config/security.xml -->
    <config>
        <provider name="chain_provider">
            <chain>
                <provider>in_memory</provider>
                <provider>user_db</provider>
            </chain>
        </provider>
        <provider name="in_memory">
            <memory>
                <user name="foo" password="test" />
            </memory>
        </provider>
        <provider name="user_db">
            <entity class="Acme\UserBundle\Entity\User" property="username" />
        </provider>
    </config>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'chain_provider' => array(
                'chain' => array(
                    'providers' => array('in_memory', 'user_db'),
                ),
            ),
            'in_memory' => array(
                'memory' => array(
                   'users' => array(
                       'foo' => array('password' => 'test'),
                   ),
                ),
            ),
            'user_db' => array(
                'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'),
            ),
        ),
    ));
    

Ora, tutti i meccanismi di autenticazione utilizzeranno il chain_provider, dal momento che è il primo specificato. Il chain_provider, a sua volta, tenta di caricare l'utente da entrambi i fornitori in_memory e user_db.

Tip

Se non ci sono ragioni per separare gli utenti in_memory dagli utenti user_db, è possibile ottenere ancora più facilmente questo risultato combinando le due sorgenti in un unico fornitore:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # app/config/security.yml
    security:
        providers:
            main_provider:
                memory:
                    users:
                        foo: { password: test }
                entity:
                    class: Acme\UserBundle\Entity\User,
                    property: username
    
  • XML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- app/config/security.xml -->
    <config>
        <provider name=="main_provider">
            <memory>
                <user name="foo" password="test" />
            </memory>
            <entity class="Acme\UserBundle\Entity\User" property="username" />
        </provider>
    </config>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'main_provider' => array(
                'memory' => array(
                    'users' => array(
                        'foo' => array('password' => 'test'),
                    ),
                ),
                'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'),
            ),
        ),
    ));
    

È anche possibile configurare il firewall o meccanismi di autenticazione individuali per utilizzare un provider specifico. Ancora una volta, a meno che un provider sia specificato esplicitamente, viene sempre utilizzato il primo fornitore:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                provider: user_db
                http_basic:
                    realm: "Demo area protetta"
                    provider: in_memory
                form_login: ~
    
  • XML
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- app/config/security.xml -->
    <config>
        <firewall name="secured_area" pattern="^/" provider="user_db">
            <!-- ... -->
            <http-basic realm="Demo area protetta" provider="in_memory" />
            <form-login />
        </firewall>
    </config>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                ...,
                'provider' => 'user_db',
                'http_basic' => array(
                    ...,
                    'provider' => 'in_memory',
                ),
                'form_login' => array(),
            ),
        ),
    ));
    

In questo esempio, se un utente cerca di accedere tramite autenticazione HTTP, il sistema di autenticazione utilizzerà il fornitore utenti in_memory. Ma se l'utente tenta di accedere tramite il form di login, sarà usato il fornitore user_db (in quanto è l'impostazione predefinita per il firewall).

Per ulteriori informazioni su fornitori utenti e configurazione del firewall, vedere il Riferimento configurazione sicurezza.

Ruoli

L'idea di un "ruolo" è la chiave per il processo di autorizzazione. A ogni utente viene assegnato un insieme di ruoli e quindi ogni risorsa richiede uno o più ruoli. Se l'utente ha i ruoli richiesti, l'accesso è concesso. In caso contrario, l'accesso è negato.

I ruoli sono abbastanza semplici e sono fondamentalmente stringhe che si possono inventare e utilizzare secondo necessità (anche se i ruoli internamente sono oggetti). Per esempio, se è necessario limitare l'accesso alla sezione admin del sito web del blog , si potrebbe proteggere quella parte con un ruolo ROLE_BLOG_ADMIN. Questo ruolo non ha bisogno di essere definito ovunque, è sufficiente iniziare a usarlo.

Note

Tutti i ruoli devono iniziare con il prefisso ROLE_ per poter essere gestiti da Symfony2. Se si definiscono i propri ruoli con una classe Role dedicata (caratteristica avanzata), non bisogna usare il prefisso ROLE_.

I ruoli gerarchici

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

  • YAML
    1
    2
    3
    4
    5
    # app/config/security.yml
    security:
        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    
  • XML
    1
    2
    3
    4
    5
    <!-- app/config/security.xml -->
    <config>
        <role id="ROLE_ADMIN">ROLE_USER</role>
        <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
    </config>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    // 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'),
        ),
    ));
    

Nella configurazione sopra, gli utenti con ruolo ROLE_ADMIN avranno anche il ruolo ROLE_USER. Il ruolo ROLE_SUPER_ADMIN ha ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH e ROLE_USER (ereditati da ROLE_ADMIN).

Logout

Generalmente, si vuole che gli utenti possano disconnettersi tramite logout. Fortunatamente, il firewall può gestire automaticamente questo caso quando si attiva il parametro di configurazione logout:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                logout:
                    path:   /logout
                    target: /
        # ...
    
  • XML
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- app/config/security.xml -->
    <config>
        <firewall name="secured_area" pattern="^/">
            <!-- ... -->
            <logout path="/logout" target="/" />
        </firewall>
        <!-- ... -->
    </config>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                // ...
                'logout' => array('path' => '/logout', 'target' => '/'),
            ),
        ),
        // ...
    ));
    

Una volta che questo viene configurato sotto il firewall, l'invio di un utente in /logout (o qualunque debba essere il percorso) farà disconnettere l'utente corrente. L'utente sarà quindi inviato alla pagina iniziale (il valore definito dal parametro target). Entrambi i parametri di configurazione path e target assumono come impostazione predefinita ciò che è specificato qui. In altre parole, se non è necessario personalizzarli, è possibile ometterli completamente e accorciare la configurazione:

  • YAML
    1
    logout: ~
    
  • XML
    1
    <logout />
    
  • PHP
    1
    'logout' => array(),
    

Si noti che non è necessario implementare un controllore per l'URL /logout, perché il firewall si occupa di tutto. Si può, tuttavia, creare una rotta da poter utilizzare per generare l'URL:

Warning

Da Symfony 2.1, occorre avere una rotta che corrisponda al percorso di logout. Senza tale rotta, il logut non funzionerà.

  • YAML
    1
    2
    3
    # app/config/routing.yml
    logout:
        path:   /logout
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    <!-- 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
    1
    2
    3
    4
    5
    6
    7
    8
    // 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;
    

Una volta che l'utente è stato disconnesso, viene rinviato al percorso definito dal parametro target sopra (ad esempio, la homepage). Per ulteriori informazioni sulla configurazione di logout, vedere il Riferimento della configurazione di sicurezza.

Controllare l'accesso nei template

Nel caso si voglia controllare all'interno di un template se l'utente corrente ha un ruolo, usare la funzione helper:

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

Note

Se si utilizza questa funzione e non si è in un URL dove c'è un firewall attivo, viene lanciata un'eccezione. Anche in questo caso, è quasi sempre una buona idea avere un firewall principale che copra tutti gli URL (come si è visto in questo capitolo).

Verifica dell'accesso nei controllori

Quando si vuole verificare se l'utente corrente abbia un ruolo nel controllore, usare il metodo isGranted() del contesto di sicurezza:

1
2
3
4
5
6
7
8
9
public function indexAction()
{
    // mostrare contenuti diversi agli utenti admin
    if($this->get('security.context')->isGranted('ADMIN')) {
        // ... caricare qui contenuti di amministrazione
    }

    // ... caricare qui altri contenuti normali
}

Note

Un firewall deve essere attivo o verrà lanciata un'eccezione quando viene chiamato il metodo isGranted. Vedere la nota precedente sui template per maggiori dettagli.

Impersonare un utente

A volte, è utile essere in grado di passare da un utente all'altro senza dover uscire e rientrare tutte le volte (per esempio quando si esegue il debug o si cerca di capire un bug che un utente vede ma che non si riesce a riprodurre). Lo si può fare facilmente, attivando l'ascoltatore switch_user del firewall:

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/security.yml
    security:
        firewalls:
            main:
                # ...
                switch_user: true
    
  • XML
    1
    2
    3
    4
    5
    6
    7
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <!-- ... -->
            <switch-user />
        </firewall>
    </config>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main'=> array(
                ...,
                'switch_user' => true
            ),
        ),
    ));
    

Per passare a un altro utente, basta aggiungere una stringa query all'URL corrente, con il parametro _switch_user e il nome utente come valore :

1
http://example.com/indirizzo?_switch_user=thomas

Per tornare indietro all'utente originale, usare il nome utente speciale _exit:

1
http://example.com/indirizzo?_switch_user=_exit

Mentre impersona, all'utente viene fornito un ruolo speciale, chiamato ROLE_PREVIOUS_ADMIN. In un template, per esempio, si può usare tale ruolo per mostrare un collegamento per tornare all'utente precedente:

  • Twig
    {% if is_granted('ROLE_PREVIOUS_ADMIN') %}
        <a href="{{ path('homepage', {_switch_user: '_exit'}) }}">Tornare all'utente precedente</a>
    {% endif %}
  • PHP
    1
    2
    3
    4
    5
    6
    7
    <?php if ($view['security']->isGranted('ROLE_PREVIOUS_ADMIN')): ?>
        <a
            href="<?php echo $view['router']->generate('homepage', array('_switch_user' => '_exit') ?>"
        >
            Tornare all'utente precedente
        </a>
    <?php endif; ?>
    

Naturalmente, questa funzionalità deve essere messa a disposizione di un piccolo gruppo di utenti. Per impostazione predefinita, l'accesso è limitato agli utenti che hanno il ruolo ROLE_ALLOWED_TO_SWITCH. Il nome di questo ruolo può essere modificato tramite l'impostazione role. Per maggiore sicurezza, è anche possibile modificare il nome del parametro della query tramite l'impostazione parameter:

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/security.yml
    security:
        firewalls:
            main:
                # ...
                switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
    
  • XML
    1
    2
    3
    4
    5
    6
    7
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <!-- ... -->
            <switch-user role="ROLE_ADMIN" parameter="_want_to_be_this_user" />
        </firewall>
    </config>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main'=> array(
                // ...
                'switch_user' => array('role' => 'ROLE_ADMIN', 'parameter' => '_want_to_be_this_user'),
            ),
        ),
    ));
    

Autenticazione senza stato

Per impostazione predefinita, Symfony2 si basa su un cookie (Session) per persistere il contesto di sicurezza dell'utente. Ma se si utilizzano certificati o l'autenticazione HTTP, per esempio, la persistenza non è necessaria, in quanto le credenziali sono disponibili a ogni richiesta. In questo caso e se non è necessario memorizzare nient'altro tra le richieste, è possibile attivare l'autenticazione senza stato (il che significa Symfony non creerà alcun cookie):

  • YAML
    1
    2
    3
    4
    5
    6
    # app/config/security.yml
    security:
        firewalls:
            main:
                http_basic: ~
                stateless:  true
    
  • XML
    1
    2
    3
    4
    5
    6
    <!-- app/config/security.xml -->
    <config>
        <firewall stateless="true">
            <http-basic />
        </firewall>
    </config>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array('http_basic' => array(), 'stateless' => true),
        ),
    ));
    

Note

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

Utilità

New in version 2.2: Le classi StringUtils e SecureRandom sono state aggiunte in Symfony 2.2

Il componente Security di Symfony dispone di una serie di utilità che riguardano la sicurezza. Queste utilità sono usate da Symfony2, ma si possono usare anche direttamente, se occorre risolvere il problemi di cui si occupano.

Confronto tra stringhe

Il tempo impiegato nel confronto tra due stringhe dipende dalle rispettive differenze. Il tempo può essere usato da un attaccante, quando le due stringhe rappresentano una password, per esempio. È noto come Timing attack.

Internamente, quando si confrontano due password, Symfony usa un algoritmo a tempo costante. Si può usare la stessa strategia nel proprio codice, grazie alla classe StringUtils:

1
2
3
4
use Symfony\Component\Security\Core\Util\StringUtils;

// password1 è uguale a password2?
$bool = StringUtils::equals($password1, $password2);

Generazione di un numero casuale

Ogni volta che occorre generare un numero casuale sicuro, si raccomanda di usare la classe SecureRandom:

1
2
3
4
use Symfony\Component\Security\Core\Util\SecureRandom;

$generator = new SecureRandom();
$random = $generator->nextBytes(10);

Il metodo nextBytes() restituisce una stringa casuale, composta dal numero di caratteri passati come parametro (10, nell'esempio appena visto).

La classe SecureRandom funziona meglio se è installato OpenSSL, ma, nel caso in cui non lo sia, si appoggia a un algoritmo interno, che ha bisogno di un file seme per funzionare. Basta passare il nome di un file, per abilitarlo:

1
2
$generator = new SecureRandom('/un/percorso/dove/memorizzare/il/seme.txt');
$random = $generator->nextBytes(10);

Note

Si può anche accedere a un'stanza di un numero casuale direttametne dal contenitore di Symfony: il suo nome è security.secure_random.

Considerazioni finali

La sicurezza può essere un problema profondo e complesso nell'applicazione da risolvere in modo corretto. Per fortuna, il componente della sicurezza di Symfony segue un ben collaudato modello di sicurezza basato su autenticazione e autorizzazione. L'autenticazione, che avviene sempre per prima, è gestita da un firewall il cui compito è quello di determinare l'identità degli utenti attraverso diversi metodi (ad esempio l'autenticazione HTTP, il form di login, ecc.). Nel ricettario, si trovano esempi di altri metodi per la gestione dell'autenticazione, includendo quello che tratta l'implementazione della funzionalità cookie "Ricorda i dati".

Una volta che un utente è autenticato, lo strato di autorizzazione può stabilire se l'utente debba o meno avere accesso a una specifica risorsa. Più frequentemente, i ruoli sono applicati a URL, classi o metodi e se l'utente corrente non ha quel ruolo, l'accesso è negato. Lo strato di autorizzazione, però, è molto più profondo e segue un sistema di "voto", in modo che tutte le parti possono determinare se l'utente corrente dovrebbe avere accesso a una data risorsa. Ulteriori informazioni su questo e altri argomenti nel ricettario.