Autenticazione degli utenti
Quasi nessuna applicazione web può fare a meno di un meccanismo per far accedere e disconnettere gli utenti e per verificarne i permessi. In questo capitolo parleremo di:
- accesso e disconnessione degli utenti
- autenticatori personalizzati
Negli esempi useremo un oggetto della classe Nette\Security\User, che rappresenta l'utente corrente e
che ottenete facendovelo passare con la dependency injection. Nei
presenter basta chiamare $user = $this->getUser().
Autenticazione
Autenticazione significa accesso dell'utente, cioè il processo con cui si verifica l'identità di un utente. L'utente
si identifica di solito con nome utente e password. La verifica la esegue il cosiddetto Autenticatore. Se l'accesso fallisce, viene lanciata una
Nette\Security\AuthenticationException.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Il nome utente o la password inseriti non sono corretti.');
}
L'utente si disconnette così:
$user->logout();
E per sapere se l'utente è connesso:
echo $user->isLoggedIn() ? 'sì' : 'no';
Molto semplice, vero? E di tutti gli aspetti di sicurezza si occupa Nette per voi.
Nei presenter potete verificare l'accesso nel metodo startup() e reindirizzare gli utenti non connessi alla pagina
di accesso.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Scadenza
L'accesso dell'utente scade insieme alla scadenza dello storage, che di solito
è la sessione (vedi l'impostazione della scadenza della sessione). Potete però
impostare anche un intervallo di tempo più breve dopo il quale l'utente viene disconnesso. A questo serve il metodo
setExpiration(), che si chiama prima di login(). Come argomento passate una stringa con un tempo
relativo:
// l'accesso scade dopo 30 minuti di inattività
$user->setExpiration('30 minutes');
// annulla la scadenza impostata
$user->setExpiration(null);
Il metodo $user->getLogoutReason() rivela se l'utente è stato disconnesso perché l'intervallo di tempo è
scaduto. Restituisce la costante Nette\Security\User::LogoutInactivity (è scaduto il limite di tempo) oppure
User::LogoutManual (è stato chiamato il metodo logout()).
Autenticatore
È un oggetto che verifica le credenziali di accesso, tipicamente nome utente e password. Una forma banale è la classe Nette\Security\SimpleAuthenticator, che si può definire nella configurazione:
security:
users:
# nome utente: password
johndoe: 'secret123'
kathy: 'evenmoresecretpassword'
Invece delle password in chiaro potete indicare anche i loro hash; vedi la configurazione.
Questa soluzione è più adatta agli scopi di prova. Vi mostreremo come creare un autenticatore che verifica le credenziali di accesso rispetto a una tabella del database.
Un autenticatore è un oggetto che implementa l'interfaccia Nette\Security\Authenticator con il metodo
authenticate(). Il suo compito è restituire un'identità oppure lanciare una
Nette\Security\AuthenticationException. Si potrebbe anche indicare un codice di errore per distinguere la situazione
in modo più fine: Authenticator::IdentityNotFound oppure Authenticator::InvalidCredential.
use Nette;
use Nette\Security\SimpleIdentity;
class MyAuthenticator implements Nette\Security\Authenticator
{
public function __construct(
private Nette\Database\Explorer $database,
private Nette\Security\Passwords $passwords,
) {
}
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->database->table('users')
->where('username', $username)
->fetch();
if (!$row) {
throw new Nette\Security\AuthenticationException('Utente non trovato.');
}
if (!$this->passwords->verify($password, $row->password)) {
throw new Nette\Security\AuthenticationException('Password non valida.');
}
return new SimpleIdentity(
$row->id,
$row->role, // oppure un array di ruoli
['name' => $row->username],
);
}
}
La classe MyAuthenticator comunica con il database tramite Nette
Database Explorer e lavora con la tabella users, dove la colonna username contiene il nome di
accesso dell'utente e la colonna password l'hash della password. Dopo aver verificato
nome e password restituisce l'identità, che contiene l'ID dell'utente, il suo ruolo (la colonna role nella tabella),
di cui parleremo più avanti, e un array con altri dati (nel nostro caso il nome
utente).
L'autenticatore lo aggiungiamo alla configurazione come servizio del container DI:
services:
- MyAuthenticator
Eventi $onLoggedIn, $onLoggedOut
L'oggetto Nette\Security\User ha gli eventi
$onLoggedIn e $onLoggedOut, quindi potete aggiungere callback che si attivano rispettivamente dopo un
accesso riuscito o dopo la disconnessione dell'utente.
$user->onLoggedIn[] = function () {
// l'utente ha appena effettuato l'accesso
};
Identità
L'identità è un insieme di informazioni sull'utente restituito dall'autenticatore, che viene poi conservato nella sessione e
si può ottenere con $user->getIdentity(). Possiamo così ottenere l'ID, i ruoli e altri dati dell'utente,
proprio come li abbiamo passati nell'autenticatore:
$user->getIdentity()->getId();
// funziona anche la scorciatoia $user->getId()
$user->getIdentity()->getRoles();
// i dati dell'utente sono accessibili come proprietà
// il nome utente che abbiamo passato in MyAuthenticator
$user->getIdentity()->name;
Cosa importante: alla disconnessione con $user->logout() l'identità non viene cancellata e resta
disponibile. Quindi, anche se un utente ha un'identità, non deve per forza essere connesso. Se vogliamo cancellare esplicitamente
l'identità, disconnettiamo l'utente chiamando logout(true).
Grazie a questo potete continuare a supporre quale utente sia al computer e mostrargli per esempio offerte personalizzate in un e-shop, ma le sue informazioni personali le potete mostrare solo dopo che ha effettuato l'accesso.
Oltre a cancellare l'identità di volta in volta con logout(true), potete disattivarne del
tutto la conservazione con la proprietà $persistIdentity. Impostata a false, l'identità viene scartata
a ogni disconnessione e alla scadenza, quindi getIdentity() restituisce poi null. La conservazione
dell'identità dipende anche dallo storage: lo storage su cookie non può conservarla dopo la disconnessione, perché cancella
sempre il cookie.
L'identità è un oggetto che implementa l'interfaccia Nette\Security\IIdentity. L'implementazione predefinita è Nette\Security\SimpleIdentity. E, come detto, viene mantenuta nella sessione, quindi se per esempio cambiamo il ruolo di uno degli utenti connessi, i vecchi dati resteranno nella sua identità finché non effettuerà di nuovo l'accesso.
Storage dell'utente connesso
Le due informazioni fondamentali sull'utente, cioè se è connesso e la sua identità, vengono di
solito trasmesse nella sessione. Il che si può cambiare. Della conservazione di queste informazioni si occupa un oggetto che
implementa l'interfaccia Nette\Security\UserStorage. Sono disponibili due implementazioni standard:
Nette\Bridges\SecurityHttp\SessionStorage, che trasmette i dati nella sessione, e CookieStorage, che li
trasmette in un cookie. Lo storage lo potete scegliere e configurare molto comodamente nella configurazione security › authentication.
Potete inoltre influire su come avvengono esattamente il salvataggio (sleep) e il ripristino (wakeup)
dell'identità. Basta che l'autenticatore implementi l'interfaccia Nette\Security\IdentityHandler. Il metodo
sleepIdentity() viene chiamato prima che l'identità venga scritta nello storage e wakeupIdentity() dopo
che è stata letta. Questi metodi possono modificare il contenuto dell'identità, oppure sostituirla con un nuovo oggetto che
restituiscono. Il metodo wakeupIdentity() può perfino restituire null, il che disconnette l'utente.
L'interfaccia dichiara anche il metodo getGuestIdentity(), vedi Identità
ospite.
Come esempio mostriamo la soluzione alla domanda frequente su come aggiornare i ruoli nell'identità subito dopo il
caricamento dalla sessione. Nel metodo wakeupIdentity() passiamo nell'identità i ruoli attuali, per esempio da un
database:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// qui potete modificare l'identità prima di scriverla nello storage dopo l'accesso,
// ma ora non ci serve
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// aggiorniamo i ruoli nell'identità
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
public function getGuestIdentity(): ?IIdentity
{
// qui non si usa alcuna identità ospite
return null;
}
Torniamo ora allo storage basato sui cookie. Permette di creare un sito in cui gli utenti possono accedere senza aver bisogno
delle sessioni. Non deve quindi scrivere su disco. È così che funziona il sito che state leggendo, forum compreso. In questo
caso l'implementazione di IdentityHandler è indispensabile. Nel cookie salveremo solo un token casuale che
rappresenta l'utente connesso.
Per prima cosa impostate nella configurazione lo storage necessario con
security › authentication › storage: cookie.
Nel database create la colonna authtoken, in cui ogni utente avrà una stringa del tutto casuale, univoca e non indovinabile di lunghezza sufficiente (almeno
13 caratteri). Il CookieStorage trasmette nel cookie solo il valore $identity->getId(), quindi in
sleepIdentity() sostituiamo l'identità originale con un'identità proxy che contiene l'authtoken
nell'ID. Al contrario, nel metodo wakeupIdentity() leggiamo dal database l'intera identità in base
all'authtoken:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function authenticate(string $username, string $password): SimpleIdentity
{
$row = $this->db->fetch('SELECT * FROM user WHERE username = ?', $username);
// verifichiamo la password
// ...
// restituiamo l'identità con tutti i dati dal database
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// restituiamo un'identità proxy in cui l'ID contiene l'authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// sostituiamo l'identità proxy con quella completa, come in authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
public function getGuestIdentity(): ?IIdentity
{
// qui non si usa alcuna identità ospite
return null;
}
}
Identità ospite
A volte torna comodo che anche i visitatori non connessi abbiano un'identità, per esempio per dare loro un insieme
predefinito di ruoli o qualche dato. Se l'autenticatore implementa IdentityHandler, può fornirne una con il metodo
getGuestIdentity(), che viene usato ogni volta che nessuno è connesso. Allora getIdentity(),
getId() e getRoles() ripiegano su di essa, così gli ospiti possono avere ruoli propri invece del
semplice ruolo guest. Restituite null se non volete un'identità ospite.
public function getGuestIdentity(): ?IIdentity
{
return new SimpleIdentity('guest', ['guest'], ['name' => 'Ospite']);
}
L'identità ospite non viene mai salvata nello storage e l'accesso la sostituisce sempre.
Più accessi indipendenti
È possibile avere più utenti che accedono in modo indipendente all'interno di uno stesso sito e di una stessa sessione contemporaneamente. Se per esempio vogliamo avere un'autenticazione separata per l'amministrazione e per la parte pubblica del sito, basta impostare per ciascuna un namespace univoco:
$user->getStorage()->setNamespace('backend');
È importante ricordarsi di impostare il namespace sempre in tutti i punti che appartengono alla parte in questione. Se usiamo i presenter, impostiamo il namespace nell'antenato comune di quella parte, di solito BasePresenter. Lo facciamo estendendo il metodo checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Se cambiate namespace durante una singola richiesta (dopo che lo stato dell'autenticazione è già stato letto), l'oggetto
User conserva ancora lo stato messo in cache dal namespace precedente. In tal caso chiamate
refreshStorage() per scartare la cache e forzare una nuova lettura dal nuovo namespace:
$user->getStorage()->setNamespace('admin');
$user->refreshStorage(); // ricarica lo stato dal nuovo namespace
Più autenticatori
Dividere un'applicazione in parti con accesso indipendente richiede di solito anche autenticatori diversi. Se però
registrassimo nella configurazione dei servizi due classi che implementano Authenticator, Nette non saprebbe quale assegnare
automaticamente all'oggetto Nette\Security\User e mostrerebbe un errore. Dobbiamo perciò limitare l'autowiring per gli autenticatori, così che funzioni solo quando qualcuno chiede
una classe concreta, per esempio FrontAuthenticator. Lo si ottiene scegliendo autowired: self:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
L'autenticatore dell'oggetto User lo impostiamo prima di chiamare il metodo login(), quindi di solito nel codice del form che esegue l'accesso:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};