Autenticazione degli utenti
Le applicazioni Web di piccole dimensioni non necessitano di alcun meccanismo per il login degli utenti o per la verifica dei loro privilegi. In questo capitolo si parlerà di:
- login e logout dell'utente
- autenticatori e autenticatori personalizzati
Negli esempi, utilizzeremo un oggetto di classe Nette\Security\User, che rappresenta l'utente corrente e
che si ottiene passandoglielo tramite dependency
injection. Nei presentatori è sufficiente chiamare $user = $this->getUser()
.
Autenticazione
Autenticazione significa accesso dell'utente, cioè il processo durante il quale viene verificata l'identità di un
utente. Di solito l'utente si identifica utilizzando nome utente e password. La verifica viene eseguita dal cosiddetto autenticatore. Se il login fallisce, viene lanciato
Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('The username or password you entered is incorrect.');
}
Ecco come disconnettere l'utente:
$user->logout();
E controllare se l'utente è connesso:
echo $user->isLoggedIn() ? 'yes' : 'no';
Semplice, no? E tutti gli aspetti della sicurezza sono gestiti da Nette per voi.
Nel presenter, è possibile verificare il login nel metodo startup()
e reindirizzare un 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 del repository, che di solito è
una sessione (vedere l'impostazione della scadenza della
sessione ). Tuttavia, si può anche impostare un intervallo di tempo più breve, dopo il quale l'utente viene disconnesso.
A questo scopo si utilizza il metodo setExpiration()
, che viene richiamato prima di login()
. Fornire
come parametro una stringa con un tempo relativo:
// il login scade dopo 30 minuti di inattività
$user->setExpiration('30 minutes');
// annullare la scadenza impostata
$user->setExpiration(null);
Il metodo $user->getLogoutReason()
indica se l'utente è stato disconnesso perché l'intervallo di tempo è
scaduto. Restituisce la costante Nette\Security\UserStorage::LogoutInactivity
se il tempo è scaduto o
UserStorage::LogoutManual
se è stato chiamato il metodo logout()
.
Autenticatore
È un oggetto che verifica i dati di accesso, cioè di solito il nome e la password. L'implementazione banale è la classe Nette\Security\SimpleAuthenticator, che può essere definita nella configurazione:
security:
users:
# name: password
johndoe: secret123
kathy: evenmoresecretpassword
Questa soluzione è più adatta a scopi di test. Verrà mostrato come creare un autenticatore che verifichi le credenziali 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 la cosiddetta identità o lanciare
un'eccezione Nette\Security\AuthenticationException
. Sarebbe anche possibile fornire un codice di errore a grana fine
Authenticator::IdentityNotFound
o 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 ruoli
['name' => $row->username],
);
}
}
La classe MyAuthenticator comunica con il database attraverso Nette
Database Explorer e lavora con la tabella users
, dove la colonna username
contiene il nome di login
dell'utente e la colonna password
contiene l 'hash. Dopo
aver verificato il nome e la password, restituisce l'identità con l'ID dell'utente, il ruolo (colonna role
della
tabella), che citeremo più avanti, e un array con dati aggiuntivi (nel nostro caso, il nome utente).
Aggiungeremo l'autenticatore alla configurazione come servizio del contenitore DI:
services:
- MyAuthenticator
$onLoggedIn, $onLoggedOut Eventi
L'oggetto Nette\Security\User
ha gli eventi
$onLoggedIn
e $onLoggedOut
, per cui è possibile aggiungere callback che vengono attivati dopo un login
riuscito o dopo il logout dell'utente.
$user->onLoggedIn[] = function () {
// l'utente ha appena effettuato l'accesso
};
Identità
Un'identità è un insieme di informazioni su un utente che viene restituito dall'autenticatore e che viene poi memorizzato in
una sessione e recuperato usando $user->getIdentity()
. Quindi possiamo ottenere l'id, i ruoli e altri dati
dell'utente come li abbiamo passati nell'autenticatore:
$user->getIdentity()->getId();
// funziona anche la scorciatoia $user->getId();
$user->getIdentity()->getRoles();
// i dati dell'utente possono essere accessibili come proprietà
// il nome che abbiamo passato in MyAuthenticator
$user->getIdentity()->name;
È importante notare che quando l'utente si disconnette usando $user->logout()
, l'identità non viene
cancellata ed è ancora disponibile. Quindi, se l'identità esiste, di per sé non garantisce che l'utente sia anche connesso.
Se si vuole cancellare esplicitamente l'identità, si effettua il logout dell'utente tramite logout(true)
.
In questo modo, è ancora possibile stabilire quale utente si trovi sul computer e, ad esempio, visualizzare offerte personalizzate nell'e-shop; tuttavia, è possibile visualizzare i suoi dati personali solo dopo aver effettuato il login.
L'identità è un oggetto che implementa l'interfaccia Nette\Security\IIdentity; l'implementazione predefinita è Nette\Security\SimpleIdentity. Come già detto, l'identità è memorizzata nella sessione, quindi se, ad esempio, cambiamo il ruolo di alcuni utenti connessi, i vecchi dati saranno conservati nell'identità fino a quando l'utente non effettuerà nuovamente il login.
Memorizzazione dell'utente registrato
Le due informazioni fondamentali sull'utente, ossia se è connesso e la sua identità, sono
solitamente contenute nella sessione. Che può essere modificata. La memorizzazione di queste informazioni è affidata a un
oggetto che implementa l'interfaccia Nette\Security\UserStorage
. Esistono due implementazioni standard, la prima
trasmette i dati in una sessione e la seconda in un cookie. Si tratta delle classi
Nette\Bridges\SecurityHttp\SessionStorage
e CookieStorage
. È possibile scegliere la memorizzazione e
configurarla in modo molto comodo nella configurazione sicurezza ›
autenticazione.
Si può anche controllare esattamente come avverrà il salvataggio (sleep) e il ripristino (wakeup)
dell'identità. Tutto ciò che serve è che l'autenticatore implementi l'interfaccia Nette\Security\IdentityHandler
.
Questa ha due metodi: sleepIdentity()
è chiamato prima che l'identità sia scritta nella memoria e
wakeupIdentity()
è chiamato dopo che l'identità è stata letta. I metodi possono modificare il contenuto
dell'identità o sostituirla con un nuovo oggetto che viene restituito. Il metodo wakeupIdentity()
può anche
restituire null
, che fa uscire l'utente.
Come esempio, mostreremo una soluzione a una domanda comune su come aggiornare i ruoli delle identità subito dopo il
ripristino da una sessione. Nel metodo wakeupIdentity()
si passano i ruoli correnti all'identità, ad esempio dal
database:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// Qui si può cambiare l'identità prima di memorizzarla 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 alla memorizzazione basata sui cookie. Consente di creare un sito web in cui gli utenti possono accedere senza
la necessità di utilizzare le sessioni. Quindi non ha bisogno di scrivere su disco. Dopo tutto, è così che funziona il sito web
che state leggendo, compreso il forum. In questo caso, l'implementazione di IdentityHandler
è una necessità. Nel
cookie verrà memorizzato solo un token casuale che rappresenta l'utente registrato.
Quindi, per prima cosa impostiamo la memorizzazione desiderata nella configurazione usando
security › authentication › storage: cookie
.
Aggiungeremo una colonna authtoken
nel database, in cui ogni utente avrà una stringa completamente casuale, unica e indovinabile di lunghezza sufficiente (almeno
13 caratteri). Il repository CookieStorage
memorizza solo il valore $identity->getId()
nel cookie,
quindi in sleepIdentity()
sostituiamo l'identità originale con un proxy con authtoken
nell'ID, mentre
nel metodo wakeupIdentity()
ripristiniamo l'intera identità dal database secondo l'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);
// verifica la password
...
// restituiamo l'identità con tutti i dati del database
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// restituiamo un'identità proxy, dove l'ID è l'authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// sostituisce l'identità proxy con un'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;
}
}
Autenticazioni multiple indipendenti
È possibile avere più utenti registrati indipendenti all'interno di un sito e una sessione alla volta. Per esempio, se si vuole avere un'autenticazione separata per il frontend e il backend, basterà impostare uno spazio dei nomi di sessione unico per ciascuno di essi:
$user->getStorage()->setNamespace('backend');
È necessario tenere presente che questo deve essere impostato in tutti i luoghi appartenenti allo stesso segmento. Quando si utilizzano i presentatori, si imposterà lo spazio dei nomi nell'antenato comune, di solito BasePresenter. Per farlo, estenderemo il metodo checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Autenticatori multipli
La suddivisione di un'applicazione in segmenti con autenticazione indipendente richiede generalmente autenticatori diversi.
Tuttavia, registrare due classi che implementano Authenticator nei servizi di configurazione darebbe luogo a un errore, perché
Nette non saprebbe quale di esse dovrebbe essere autocablata all'oggetto Nette\Security\User
. Per
questo motivo, dobbiamo limitare il cablaggio automatico con autowired: self
, in modo che venga attivato solo quando
la classe viene richiesta in modo specifico:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Dobbiamo impostare il nostro autenticatore sull'oggetto User solo prima di chiamare il metodo login(), il che di solito significa nel callback del form di login:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};