Autentificarea utilizatorilor

Aproape nicio aplicație web nu se poate lipsi de un mecanism de autentificare a utilizatorilor și de verificare a permisiunilor utilizatorilor. În acest capitol vom vorbi despre:

  • autentificarea și deconectarea utilizatorilor
  • autentificatoare personalizate

Instalare și cerințe

În exemple vom folosi obiectul clasei Nette\Security\User, care reprezintă utilizatorul curent și la care ajungeți solicitându-l prin injecția de dependențe. În presenteri este suficient doar să apelați $user = $this->getUser().

Autentificare

Autentificarea se referă la conectarea utilizatorilor, adică procesul prin care se verifică dacă utilizatorul este într-adevăr cine pretinde a fi. De obicei, se dovedește prin nume de utilizator și parolă. Verificarea este efectuată de așa-numitul autentikátor. Dacă autentificarea eșuează, se aruncă Nette\Security\AuthenticationException.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('Numele de utilizator sau parola este incorectă');
}

În acest mod deconectați utilizatorul:

$user->logout();

Și verificarea dacă este conectat:

echo $user->isLoggedIn() ? 'da' : 'nu';

Foarte simplu, nu-i așa? Și toate aspectele de securitate sunt gestionate de Nette pentru dumneavoastră.

În presenteri puteți verifica autentificarea în metoda startup() și redirecționa utilizatorul neconectat către pagina de autentificare.

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isLoggedIn()) {
		$this->redirect('Sign:in');
	}
}

Expirare

Autentificarea utilizatorului expiră odată cu expirarea stocării, care este de obicei sesiunea (vezi setarea expirarea sesiunii). Se poate seta însă și un interval de timp mai scurt, după expirarea căruia utilizatorul va fi deconectat. Pentru aceasta servește metoda setExpiration(), care se apelează înainte de login(). Ca parametru, introduceți un șir cu timpul relativ:

// autentificarea expiră după 30 de minute de inactivitate
$user->setExpiration('30 minutes');

// anularea expirării setate
$user->setExpiration(null);

Dacă utilizatorul a fost deconectat din cauza expirării intervalului de timp, o indică metoda $user->getLogoutReason(), care returnează fie constanta Nette\Security\UserStorage::LogoutInactivity (a expirat limita de timp), fie UserStorage::LogoutManual (deconectat prin metoda logout()).

Autentificator

Este un obiect care verifică datele de autentificare, adică de obicei numele și parola. O formă trivială este clasa Nette\Security\SimpleAuthenticator, pe care o putem defini în configurație:

security:
	users:
		# nume: parola
		frantisek: parolasecreta
		katka: parolasiamaisecreta

Această soluție este potrivită mai degrabă pentru scopuri de testare. Vom arăta cum să creăm un autentificator care va verifica datele de autentificare față de un tabel din baza de date.

Autentificatorul este un obiect care implementează interfața Nette\Security\Authenticator cu metoda authenticate(). Sarcina sa este fie să returneze așa-numita identitate, fie să arunce excepția Nette\Security\AuthenticationException. Ar fi posibil să se specifice și un cod de eroare pentru o diferențiere mai fină a situației apărute: Authenticator::IdentityNotFound și 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('User not found.');
		}

		if (!$this->passwords->verify($password, $row->password)) {
			throw new Nette\Security\AuthenticationException('Invalid password.');
		}

		return new SimpleIdentity(
			$row->id,
			$row->role, // sau un array cu mai multe roluri
			['name' => $row->username],
		);
	}
}

Clasa MyAuthenticator comunică cu baza de date prin intermediul Nette Database Explorer și lucrează cu tabelul users, unde în coloana username se află numele de utilizator și în coloana password amprenta parolei. După verificarea numelui și parolei, returnează identitatea, care conține ID-ul utilizatorului, rolul său (coloana role din tabel), despre care vom vorbi mai mult mai târziu, și un array cu date suplimentare (în cazul nostru, numele de utilizator).

Vom adăuga autentificatorul în configurație ca serviciu al containerului DI:

services:
	- MyAuthenticator

Evenimentele $onLoggedIn, $onLoggedOut

Obiectul Nette\Security\User are evenimente $onLoggedIn și $onLoggedOut, puteți deci adăuga callback-uri care se vor invoca după autentificarea cu succes, respectiv după deconectarea utilizatorului.

$user->onLoggedIn[] = function () {
	// utilizatorul tocmai a fost autentificat
};

Identitate

Identitatea reprezintă un set de informații despre utilizator, returnat de autentificator și care se păstrează ulterior în sesiune și îl obținem folosind $user->getIdentity(). Putem deci obține id-ul, rolurile și alte date ale utilizatorului, așa cum le-am transmis în autentificator:

$user->getIdentity()->getId();
// funcționează și scurtătura $user->getId();

$user->getIdentity()->getRoles();

// datele utilizatorului sunt disponibile ca proprietăți
// numele pe care l-am transmis în MyAuthenticator
$user->getIdentity()->name;

Ceea ce este important este că la deconectarea folosind $user->logout(), identitatea nu se șterge și este în continuare disponibilă. Deci, deși utilizatorul are o identitate, nu trebuie să fie conectat. Dacă am dori să ștergem explicit identitatea, deconectăm utilizatorul apelând logout(true).

Datorită acestui fapt, puteți în continuare presupune ce utilizator este la calculator și, de exemplu, să îi afișați oferte personalizate în e-shop, însă afișarea datelor sale personale o puteți face doar după autentificare.

Identitatea este un obiect care implementează interfața Nette\Security\IIdentity, implementarea implicită fiind Nette\Security\SimpleIdentity. Și, așa cum s-a menționat, se menține în sesiune, deci dacă, de exemplu, schimbăm rolul unuia dintre utilizatorii conectați, datele vechi rămân în identitatea sa până la următoarea sa autentificare.

Stocarea utilizatorului autentificat

Două informații de bază despre utilizator, adică dacă este conectat și #identitate sa, se transmit de obicei în sesiune. Ceea ce poate fi schimbat. Stocarea acestor informații este responsabilitatea unui obiect care implementează interfața Nette\Security\UserStorage. Sunt disponibile două implementări standard, prima transmite datele în sesiune și a doua în cookie. Este vorba despre clasele Nette\Bridges\SecurityHttp\SessionStorage și CookieStorage. Puteți alege stocarea și o puteți configura foarte comod în configurația security › authentication.

Mai departe, puteți influența exact cum va decurge stocarea identității (sleep) și restaurarea (wakeup). Este suficient ca autentificatorul să implementeze interfața Nette\Security\IdentityHandler. Aceasta are două metode: sleepIdentity() se apelează înainte de scrierea identității în stocare și wakeupIdentity() după citirea acesteia. Metodele pot modifica conținutul identității, eventual o pot înlocui cu un obiect nou pe care îl returnează. Metoda wakeupIdentity() poate chiar returna null, ceea ce îl deconectează pe utilizator.

Ca exemplu, vom arăta soluția la întrebarea frecventă despre cum să actualizăm rolurile în identitate imediat după încărcarea din sesiune. În metoda wakeupIdentity() vom transmite în identitate rolurile actuale, de exemplu, din baza de date:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// aici se poate modifica identitatea înainte de scrierea în stocare după autentificare,
		// dar acum nu avem nevoie de asta
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// actualizarea rolurilor în identitate
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

Și acum ne întoarcem la stocarea bazată pe cookie-uri. Vă permite să creați un site web unde utilizatorii se pot autentifica fără a avea nevoie de sesiuni. Adică nu are nevoie să scrie pe disc. De altfel, așa funcționează și site-ul pe care îl citiți acum, inclusiv forumul. În acest caz, implementarea IdentityHandler este o necesitate. În cookie vom stoca doar un token aleatoriu reprezentând utilizatorul conectat.

Mai întâi, deci, în configurație setăm stocarea dorită folosind security › authentication › storage: cookie.

În baza de date vom crea o coloană authtoken, în care fiecare utilizator va avea un șir complet aleatoriu, unic și imposibil de ghicit de lungime suficientă (cel puțin 13 caractere). Stocarea CookieStorage transmite în cookie doar valoarea $identity->getId(), așa că în sleepIdentity() vom înlocui identitatea originală cu una substitutivă cu authtoken în ID, în schimb în metoda wakeupIdentity() vom citi întreaga identitate din baza de date pe baza authtoken-ului:

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);
		// verificăm parola
		...
		// returnăm identitatea cu toate datele din baza de date
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// returnăm o identitate substitutivă, unde în ID va fi authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// înlocuim identitatea substitutivă cu identitatea completă, ca în authenticate()
		$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
		return $row
			? new SimpleIdentity($row->id, null, (array) $row)
			: null;
	}
}

Mai multe autentificări independente

Este posibil să aveți mai mulți utilizatori autentificați independent în cadrul aceluiași site web și al aceleiași sesiuni. Dacă, de exemplu, dorim să avem o autentificare separată pentru administrare și partea publică pe site, este suficient să setăm un nume propriu pentru fiecare dintre ele:

$user->getStorage()->setNamespace('backend');

Este important să ne amintim să setăm spațiul de nume întotdeauna în toate locurile care aparțin părții respective. Dacă folosim presenteri, setăm spațiul de nume în strămoșul comun pentru partea respectivă – de obicei BasePresenter. Facem acest lucru extinzând metoda checkRequirements():

public function checkRequirements($element): void
{
	$this->getUser()->getStorage()->setNamespace('backend');
	parent::checkRequirements($element);
}

Mai mulți autentificatori

Împărțirea aplicației în părți cu autentificare independentă necesită de obicei și autentificatori diferiți. Cu toate acestea, de îndată ce am înregistra două clase care implementează Authenticator în configurația serviciilor, Nette nu ar ști pe care dintre ele să o atribuie automat obiectului Nette\Security\User și ar afișa o eroare. De aceea, trebuie să limităm autowiring-ul pentru autentificatori astfel încât să funcționeze doar atunci când cineva solicită o clasă specifică, de exemplu, FrontAuthenticator, ceea ce realizăm alegând autowired: self:

services:
	-
		create: FrontAuthenticator
		autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FrontAuthenticator $authenticator,
	) {
	}
}

Setăm autentificatorul obiectului User înainte de a apela metoda login(), deci de obicei în codul formularului care îl autentifică:

$form->onSuccess[] = function (Form $form, \stdClass $data) {
	$user = $this->getUser();
	$user->setAuthenticator($this->authenticator);
	$user->login($data->username, $data->password);
	// ...
};
versiune: 4.0