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 sia l’utente.

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

images/book/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 Security </components/security/introduction> 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 Security 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):

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.

Tip

Si può anche far corrispondere una richiesta in base ad altri dettagli (p.e. l’host o il metodo). Per maggiori informazioni ed esempi, leggere /cookbook/security/firewall_restriction.

images/book/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/book/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/book/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/book/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 /cookbook/security/entity_provider. 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:

Tip

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

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):

Note

Non è necessario implementare un controllore per l’URL /login_check perché il firewall catturerà ed elaborerà qualunque form inviato a questo URL. Tuttuavia, occorre avere una rotta (come mostrato qui) per questo URL, come anche per il percorso di logout (vedere Logging Out).

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:

// src/Acme/SecurityBundle/Controller/SecurityController.php;
namespace Acme\SecurityBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContextInterface;

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

        // verifica di eventuali errori
        if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(
                SecurityContextInterface::AUTHENTICATION_ERROR
            );
        } elseif (null !== $session && $session->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
            $error = $session->get(SecurityContextInterface::AUTHENTICATION_ERROR);
            $session->remove(SecurityContextInterface::AUTHENTICATION_ERROR);
        } else {
            $error = '';
        }

        // ultimo nome utente inserito
        $lastUsername = (null === $session) ? '' : $session->get(SecurityContextInterface::LAST_USERNAME);

        return $this->render(
            'AcmeSecurityBundle:Security:login.html.twig',
            array(
                // ultimo nome utente inserito
                'last_username' => $lastUsername,
                '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 compito dello sviluppatore è 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:

Caution

Questo form di login al momento non è protetto da attacchi CSRF. Leggere /cookbook/security/csrf_in_login_form per sapere come proteggere i form.

Tip

La variabile error passata nel template è un’istanza di Symfony\Component\Security\Core\Exception\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/pippo). 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 /cookbook/security/form_login.

Autorizzazione

Il primo passo per la sicurezza è sempre l’autenticazione. 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.

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

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:

  1. se la richiesta in arrivo deve corrispondere a questa voce di controllo di accesso
  2. una volta corrisposta, se alcune restrizioni di accesso debbano essere applicate:

1. Opzioni di corrispondenza

Symfony2 crea un’istanza di Symfony\Component\HttpFoundation\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 o ips
  • host
  • methods

Si prende il seguente access_control come esempio:

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.

2. Controllo dell’accesso

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

  • role Se l’utente non ha il ruolo fornito, l’accesso viene negato (internamente, viene lanciata Symfony\Component\Security\Core\Exception\AccessDeniedException);
  • allow_if Se l’espressione restituisce false, l’accesso viene negato;
  • 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 /cookbook/controller/error_pages 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.

New in version 2.3: La versione 2.3 consente più indirizzi IP in una singola regola, con il costrutto ips: [a, b]. Prima della 2.3, era necessario creare una regola per ogni indirizzo IP e usare la chiave ip al posto di ips.

Caution

Come si può vedere nella spiegazione sotto all’esempio, l’opzione ip non limita a uno specifico indirizzo IP. Invece, usando la chiave ip si ottiene che la voce access_control avrà una corrispondenza solo per il corrispondente indirizzo IP e gli utenti che accedono da diversi indirizzi IP continueranno nelle successive voci dell’elenco acces_control.

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

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 o da ::1 (l’indirizzo locale in IPv6):

  • 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 espressione

New in version 2.4: La funzionalità allow_if è stata introdotta in Symfony 2.4.

Una volta corrisposta una voce access_control, si può negare l’accesso tramite la chiave roles oppure usare una logica più complessa, con un’espressione nella chiave allow_if:

In questo caso, quando l’utente prova ad accedere a un URL che inizia per /_internal/secure, gli sarà consentito l’accesso solo se l’indirizzo IP è 127.0.0.1 o se l’utente ha il ruolo ROLE_ADMIN.

All’interno dell’espressione, si ha accesso a diverse variabili e funzioni, inclusa request, che è l’oggetto Symfony\Component\HttpFoundation\Request di Symfony (vedere component-http-foundation-request).

Per una lista di altre funzioni e variabili, vedere funzioni e variabili.

Forzare un canale (http, https)

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. Se tale access_control trova corrispondenza e la richiesta usa il canale http, l’utente sarà rinviato a 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:

// ...

public function helloAction($name)
{
    if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
        throw $this->createAccessDeniedException('Unable to access this page!');
    }

    // ...
}

New in version 2.5: Il metodo createAccessDeniedException è stato introdotto in Symfony 2.5.

Il metodo :method:`Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller::createAccessDeniedException` crea un oggetto speciale Symfony\Component\Security\Core\Exception\AccessDeniedException, che alla fine lancia una risposta HTTP 403 in Symfony.

Con SensioFrameworkExtraBundle, si possono anche proteggere i controllori 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.

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 Security per proteggere servizi e metodi diversi nell’applicazione, vedere /cookbook/security/securing_services.

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 Security 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: /cookbook/security/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? (fornitori di utenti)

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 dispone dei due fornitori di 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.

Questo fornitore di utenti è chiamato “in-memory” , dal momento che gli utenti non sono memorizzati in una base dati. L’oggetto utente effettivo è fornito da Symfony (Symfony\Component\Security\Core\User\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:

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.

// 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 Symfony\Component\Security\Core\User\UserInterface. Questo significa che il concetto di “utente” può essere qualsiasi cosa, purché implementi questa interfaccia.

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:

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 /cookbook/security/entity_provider.

Per ulteriori informazioni sulla creazione di un fornitore personalizzato (ad esempio se è necessario caricare gli utenti tramite un servizio web), vedere /cookbook/security/custom_provider.

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 bcrypt, si può fare come segue:

Ora si può calcolare la password cifrata, manualmente (p.e. password_hash('ryanpass', PASSWORD_BCRYPT, array('cost' => 12));) oppure usando uno strumento online.

Gli algoritmi supportati da questo metodo dipendono dalla versione di PHP. Un elenco completo è disponibile richiamando la funzione :phpfunction:`hash_algos`.

Tip

Si possono anche usare diversi algoritmi di hash per utenti diversi. Vedere /cookbook/security/named_encoders per maggiori dettagli.

Determinare la password con hash

Se si memorizzano gli utenti sulla base dati e 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 prima di inserirlo. Indipendentemente dall’algoritmo configurato per l’oggetto User, l’hash della password può essere determinato nel seguente modo da un controllore:

$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);

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

Caution

Quando si consente all’utente di inviare una password in chiaro (p.e. form di registrazione, form di cambio password), occorre avere una validazione, che garantisca che la password non superi i 4096 caratteri. Per maggiori dettagli, vedere implementare un semplice form di registrazione.

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:

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

In un controllore, si può usare una scorciatoia:

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 :method:`GlobalVariables::getUser() <Symfony\\Bundle\\FrameworkBundle\\Templating\\GlobalVariables::getUser>`:

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:

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.

È 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:

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 /reference/configuration/security.

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:

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).

Verifica dell’accesso

Una volta che si dispone di utenti e di ruoli, si può andare oltre l’autorizzazione basata su schemi di URL.

Verifica dell’accesso nei controllori

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:

// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

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

    // ...
}

Caution

Un firewall deve essere attivo o verrà lanciata un’eccezione quando viene chiamato il metodo isGranted. Spesso è una buona idea avere un firewall principale, che copra tutti gli URL (come mostrato in questo capitolo).

Controlli di accesso complessi con espressioni

New in version 2.4: La funzionalità delle espressioni è stata introdotta in Symfony 2.4.

Oltre a un ruolo, come ROLE_ADMIN, il metodo isGranted accetta anche un oggetto Symfony\Component\ExpressionLanguage\Expression:

use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\ExpressionLanguage\Expression;
// ...

public function indexAction()
{
    if (!$this->get('security.context')->isGranted(new Expression(
        '"ROLE_ADMIN" in roles or (user and user.isSuperAdmin())'
    ))) {
        throw new AccessDeniedException();
    }

    // ...
}

In questo esempio, se l’utente ha ROLE_ADMIN o se il metodo isSuperAdmin() dell’oggetto utente restituisce true, sarà garantito l’accesso (nota: l’oggetto User potrebbe non avere un metodo isSuperAdmin, tale metodo è stato inventato per questo esempio).

Si può approfondire la sintassi del linguaggio delle espressioni in /components/expression_language/syntax.

All’interno dell’espressione si ha accesso a diverse variabili:

  • user L’oggetto utente (o la stringa anon se non autenticato);
  • roles L’array di ruoli dell’utente, inclusi quelli provenienti dalla gerarchia dei ruoli ma esclusi gli attributi IS_AUTHENTICATED_* (vedere le funzioni, qui sotto);
  • object: L’eventuale oggetto passato come secondo parametro a isGranted ;
  • token L’oggetto token;
  • trust_resolver: L’oggetto Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface: probabilmente si useranno le funzioni is_* al suo posto.

Inoltre, si ha accesso a varie funzioni:

  • is_authenticated: Restituisce true se l’utente è autenticato tramite “ricordami” o autenticato “pienamente”, in pratica dice se l’utente è entrato;
  • is_anonymous: Equivalente all’uso di IS_AUTHENTICATED_ANONYMOUSLY con la funzione isGranted;
  • is_remember_me: Simile, ma non uguale a IS_AUTHENTICATED_REMEMBERED, vedere sotto;
  • is_fully_authenticated: Simile, ma non uguale a IS_AUTHENTICATED_FULLY, vedere sotto;
  • has_role: Verifica se l’utente ha il ruolo dato, equivalente a un’espressione come 'ROLE_ADMIN' in roles.

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 Security per proteggere servizi e metodi diversi nell’applicazione, vedere /cookbook/security/securing_services.

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 aiutante:

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).

New in version 2.4: La funzionalità expression è stata introdotta in Symfony 2.4.

Si possono anche usare espressioni all’interno dei template:

Per maggiori dettagli su espressioni e sicurezza, vedere Controlli di accesso complessi con espressioni.

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 Security 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: /cookbook/security/acl.

Logging Out

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:

Una volta inserita questa condigurazione in un firewall, inviare un utente a /logout (o a un altro percorso configurato in path) lo farà uscire dall’autenticazione. L’utente sarà quindi rinviato alla pagina iniziale (il valore definito nel parametro target). Entrambi i parametri path e target hanno come valore predefinito quello specificato qui. In altre parole, a meno che non si desideri personalizzarli, possono essere omessi e quindi abbreviare la configurazione:

Si noti che non occorre implementare un controllore per l’URL /logout, perché il firewall se ne occuperà. Tuttavia, occorre creare una rotta, in modo da poterla usare per generare l’URL:

Una volta che l’utente sia uscito, sarà rinviato al percorso definito in target (per esempio alla pagina iniziale). Per maggiori informazioni, vedere il riferimento alla configurazione della sicurezza.

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):

Note

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

Utilità

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 codice, grazie alla classe Symfony\Component\Security\Core\Util\StringUtils:

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 Symfony\Component\Security\Core\Util\SecureRandom:

use Symfony\Component\Security\Core\Util\SecureRandom;

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

Il metodo :method:`Symfony\\Component\\Security\\Core\\Util\\SecureRandom::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:

$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 Security 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.

Saperne di più con il ricettario

  • Forzare HTTP/HTTPS
  • Impersonare un utente
  • Blacklist di utenti per indirizzo IP
  • Access Control List (ACL)
  • /cookbook/security/remember_me
  • How to Restrict Firewalls to a Specific Host