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

Installazione e requisiti

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);
	// ...
};
versione: 4.0