Login utente (Autenticazione)
Quasi nessuna applicazione web può fare a meno di un meccanismo di login utente e di verifica dei permessi utente. In questo capitolo parleremo di:
- login e logout degli utenti
- autenticatori personalizzati
Negli esempi useremo l'oggetto della classe Nette\Security\User, che rappresenta l'utente corrente e
al quale potete accedere facendovelo passare tramite dependency injection. Nei presenter basta solo
chiamare $user = $this->getUser()
.
Autenticazione
Per autenticazione si intende il login degli utenti, ovvero il processo durante il quale si verifica se l'utente è
davvero chi dice di essere. Di solito si dimostra con nome utente e password. La verifica viene eseguita dal cosiddetto autenticatore. Se il login fallisce, viene lanciata
Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Nome utente o password non corretti');
}
In questo modo si effettua il logout dell'utente:
$user->logout();
E per verificare se è loggato:
echo $user->isLoggedIn() ? 'sì' : 'no';
Molto semplice, vero? E tutti gli aspetti di sicurezza li gestisce Nette per voi.
Nei presenter potete verificare il login nel metodo startup()
e reindirizzare l'utente non loggato alla pagina
di login.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Scadenza
Il login dell'utente scade insieme alla scadenza dello storage, che di
solito è la sessione (vedi impostazione scadenza sessione).
È però possibile impostare anche un intervallo di tempo più breve, trascorso il quale l'utente viene disconnesso. A questo
serve il metodo setExpiration()
, che viene chiamato prima di login()
. Come parametro indicate una
stringa con il tempo relativo:
// il login scadrà dopo 30 minuti di inattività
$user->setExpiration('30 minutes');
// annullamento della scadenza impostata
$user->setExpiration(null);
Se l'utente è stato disconnesso a causa della scadenza dell'intervallo di tempo, lo rivela il metodo
$user->getLogoutReason()
, che restituisce o la costante Nette\Security\UserStorage::LogoutInactivity
(limite di tempo scaduto) o UserStorage::LogoutManual
(disconnesso dal metodo logout()
).
Autenticatore
Si tratta di un oggetto che verifica le credenziali di accesso, ovvero di solito nome e password. Una forma banale è la classe Nette\Security\SimpleAuthenticator, che possiamo definire nella configurazione:
security:
users:
# nome: password
francesco: passwordsegreta
caterina: passwordancorapiusegreta
Questa soluzione è adatta piuttosto per scopi di test. Vediamo come creare un autenticatore che verifichi le credenziali di accesso rispetto a una tabella del database.
L'autenticatore è un oggetto che implementa l'interfaccia Nette\Security\Authenticator con il metodo
authenticate()
. Il suo compito è restituire la cosiddetta identità o lanciare
un'eccezione Nette\Security\AuthenticationException
. Sarebbe possibile indicare anche un codice di errore per
distinguere più finemente la situazione verificatasi: Authenticator::IdentityNotFound
e
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, // o array di più ruoli
['name' => $row->username],
);
}
}
La classe MyAuthenticator comunica con il database tramite Nette Database
Explorer e lavora con la tabella users
, dove nella colonna username
c'è il nome di login
dell'utente e nella colonna password
l'hash della password.
Dopo aver verificato nome e password, restituisce l'identità, che contiene l'ID dell'utente, il suo ruolo (colonna
role
nella tabella), di cui parleremo più avanti, e un array con altri dati (nel nostro caso
il nome utente).
Aggiungiamo ancora l'autenticatore alla configurazione come servizio del container DI:
services:
- MyAuthenticator
Eventi $onLoggedIn, $onLoggedOut
L'oggetto Nette\Security\User
ha eventi
$onLoggedIn
e $onLoggedOut
, potete quindi aggiungere callback che vengono invocati dopo il login
riuscito rispettivamente dopo il logout dell'utente.
$user->onLoggedIn[] = function () {
// l'utente è stato appena loggato
};
Identità
L'identità rappresenta un insieme di informazioni sull'utente, restituito dall'autenticatore e che viene successivamente
conservato nella sessione e ottenuto tramite $user->getIdentity()
. Possiamo quindi ottenere l'id, i ruoli e altri
dati utente, così come li abbiamo passati nell'autenticatore:
$user->getIdentity()->getId();
// funziona anche la scorciatoia $user->getId();
$user->getIdentity()->getRoles();
// i dati utente sono disponibili come proprietà
// nome, che abbiamo passato in MyAuthenticator
$user->getIdentity()->name;
Ciò che è importante è che al momento del logout tramite $user->logout()
l'identità non viene
cancellata ed è ancora disponibile. Quindi, anche se l'utente ha un'identità, non deve necessariamente essere loggato. Se
volessimo cancellare esplicitamente l'identità, effettueremo il logout dell'utente chiamando logout(true)
.
Grazie a ciò potete continuare a presumere quale utente sia al computer e ad esempio mostrargli offerte personalizzate nell'e-shop, tuttavia potete visualizzare i suoi dati personali solo dopo il login.
L'identità è un oggetto che implementa l'interfaccia Nette\Security\IIdentity, l'implementazione predefinita è Nette\Security\SimpleIdentity. E come accennato, viene mantenuta nella sessione, quindi se ad esempio cambiamo il ruolo di uno degli utenti loggati, i vecchi dati rimarranno nella sua identità fino al suo successivo login.
Storage dell'utente loggato
Le due informazioni fondamentali sull'utente, ovvero se è loggato e la sua identita, vengono di
solito trasmesse nella sessione. Ciò può essere modificato. La memorizzazione di queste informazioni è gestita da un oggetto
che implementa l'interfaccia Nette\Security\UserStorage
. Sono disponibili due implementazioni standard, la prima
trasmette i dati nella sessione e la seconda in un cookie. Si tratta delle classi
Nette\Bridges\SecurityHttp\SessionStorage
e CookieStorage
. Potete scegliere lo storage e configurarlo
molto comodamente nella configurazione security ›
authentication.
Inoltre, potete influenzare come avverrà esattamente la memorizzazione dell'identità (sleep) e il ripristino
(wakeup). Basta che l'autenticatore implementi l'interfaccia Nette\Security\IdentityHandler
. Questa ha due
metodi: sleepIdentity()
viene chiamato prima della scrittura dell'identità nello storage e
wakeupIdentity()
dopo la sua lettura. I metodi possono modificare il contenuto dell'identità, oppure sostituirla
con un nuovo oggetto che restituiscono. Il metodo wakeupIdentity()
può persino restituire null
,
disconnettendo così l'utente.
Come esempio, mostreremo la soluzione a una domanda frequente, ovvero come aggiornare i ruoli nell'identità subito dopo il
caricamento dalla sessione. Nel metodo wakeupIdentity()
passiamo all'identità i ruoli attuali, ad esempio dal
database:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// qui si può modificare l'identità prima della scrittura nello storage dopo il login,
// ma ora non ne abbiamo bisogno
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// aggiornamento dei ruoli nell'identità
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
E ora torniamo allo storage basato sui cookie. Vi permette di creare un sito web dove gli utenti possono loggarsi senza bisogno
di sessioni. Quindi non ha bisogno di scrivere sul disco. Del resto, così funziona anche il sito che state leggendo, compreso il
forum. In questo caso, l'implementazione di IdentityHandler
è una necessità. Nel cookie, infatti, memorizzeremo
solo un token casuale che rappresenta l'utente loggato.
Prima di tutto, quindi, nella configurazione impostiamo lo storage desiderato tramite
security › authentication › storage: cookie
.
Nel database creiamo una colonna authtoken
, in cui ogni utente avrà una stringa completamente casuale, unica e non indovinabile di lunghezza sufficiente (almeno
13 caratteri). Lo storage CookieStorage
trasmette nel cookie solo il valore $identity->getId()
,
quindi in sleepIdentity()
sostituiamo l'identità originale con una sostitutiva con authtoken
nell'ID,
al contrario nel metodo wakeupIdentity()
leggiamo l'intera identità dal database 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à sostitutiva, dove nell'ID ci sarà l'authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// sostituiamo l'identità sostitutiva con l'identità 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;
}
}
Più login indipendenti
È possibile avere contemporaneamente, all'interno dello stesso sito web e della stessa sessione, più utenti loggati indipendentemente. Se ad esempio vogliamo avere sul sito un'autenticazione separata per l'amministrazione e la parte pubblica, basta impostare per ognuna un proprio nome:
$user->getStorage()->setNamespace('backend');
È importante ricordare di impostare sempre il namespace in tutti i punti appartenenti alla parte specifica. Se usiamo i presenter, impostiamo il namespace nel predecessore comune per quella parte – di solito BasePresenter. Lo facciamo estendendo il metodo checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Più autenticatori
La divisione dell'applicazione in parti con login indipendente richiede di solito anche autenticatori diversi. Tuttavia, non
appena 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. Pertanto, dobbiamo limitare l'autowiring per gli autenticatori in modo che funzioni solo
quando qualcuno richiede una classe specifica, ad esempio FrontAuthenticator, cosa che otteniamo scegliendo
autowired: self
:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Impostiamo l'autenticatore dell'oggetto User prima di chiamare il metodo login(), quindi di solito nel codice del form che lo logga:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};