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

Installazione e requisiti

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