Felhasználói bejelentkezés (Authentikáció)

Szinte egyetlen webalkalmazás sem nélkülözheti a felhasználói bejelentkezési mechanizmust és a felhasználói jogosultságok ellenőrzését. Ebben a fejezetben a következőkről lesz szó:

  • felhasználók be- és kijelentkeztetése
  • saját authentikátorok

Telepítés és követelmények

A példákban a Nette\Security\User osztály objektumát fogjuk használni, amely az aktuális felhasználót képviseli, és amelyhez úgy juthat hozzá, hogy dependency injection segítségével kéri át. A presenterekben elegendő csak a $user = $this->getUser() hívása.

Authentikáció

Az authentikáció felhasználói bejelentkezést jelent, tehát azt a folyamatot, amely során ellenőrizzük, hogy a felhasználó valóban az-e, akinek kiadja magát. Általában felhasználónévvel és jelszóval igazolja magát. Az ellenőrzést az ún. autentikátor végzi. Ha a bejelentkezés sikertelen, Nette\Security\AuthenticationException kivétel dobódik.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('A felhasználónév vagy jelszó helytelen');
}

Így jelentkezteti ki a felhasználót:

$user->logout();

És annak megállapítása, hogy be van-e jelentkezve:

echo $user->isLoggedIn() ? 'igen' : 'nem';

Nagyon egyszerű, ugye? És minden biztonsági szempontot a Nette kezel Ön helyett.

A presenterekben ellenőrizheti a bejelentkezést a startup() metódusban, és a be nem jelentkezett felhasználót átirányíthatja a bejelentkezési oldalra.

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

Lejárat

A felhasználó bejelentkezése a tároló lejáratával együtt jár le, amely általában a session (lásd a session lejárata beállítását). De beállítható rövidebb időintervallum is, amelynek lejárta után a felhasználó kijelentkezik. Erre szolgál a setExpiration() metódus, amelyet a login() előtt kell meghívni. Paraméterként adjon meg egy relatív időt tartalmazó stringet:

// a bejelentkezés 30 perc inaktivitás után lejár
$user->setExpiration('30 minutes');

// a beállított lejárat törlése
$user->setExpiration(null);

Azt, hogy a felhasználó az időintervallum lejárta miatt jelentkezett-e ki, a $user->getLogoutReason() metódus árulja el, amely vagy a Nette\Security\UserStorage::LogoutInactivity konstanst (lejárt az időkorlát) vagy a UserStorage::LogoutManual konstanst (a logout() metódussal jelentkeztették ki) adja vissza.

Authentikátor

Ez egy objektum, amely ellenőrzi a bejelentkezési adatokat, tehát általában a nevet és a jelszót. Triviális formája a Nette\Security\SimpleAuthenticator osztály, amelyet a konfigurációban definiálhatunk:

security:
	users:
		# név: jelszó
		frantisek: tajneheslo
		katka: jestetajnejsiheslo

Ez a megoldás inkább tesztelési célokra alkalmas. Megmutatjuk, hogyan hozzunk létre egy authentikátort, amely egy adatbázis tábla alapján ellenőrzi a bejelentkezési adatokat.

Az authentikátor egy objektum, amely implementálja a Nette\Security\Authenticator interfészt a authenticate() metódussal. Feladata vagy az ún. identitást visszaadni, vagy Nette\Security\AuthenticationException kivételt dobni. Lehetőség lenne még hibakódot is megadni a helyzet finomabb megkülönböztetésére: Authenticator::IdentityNotFound és 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, // vagy több szerepkör tömbje
			['name' => $row->username],
		);
	}
}

A MyAuthenticator osztály a Nette Database Explorer segítségével kommunikál az adatbázissal, és a users táblával dolgozik, ahol a username oszlopban a felhasználó bejelentkezési neve, a password oszlopban pedig a jelszó lenyomatát tárolja. A név és jelszó ellenőrzése után visszaadja az identitást, amely tartalmazza a felhasználó azonosítóját (ID), szerepkörét (a tábla role oszlopa), amelyről később többet mondunk, és egy tömböt további adatokkal (esetünkben a felhasználónévvel).

Az authentikátort még hozzáadjuk a DI konténer konfigurációjához szolgáltatásként:

services:
	- MyAuthenticator

$onLoggedIn, $onLoggedOut események

A Nette\Security\User objektumnak vannak események $onLoggedIn és $onLoggedOut, tehát hozzáadhat callbackeket, amelyek a sikeres bejelentkezés után, illetve a felhasználó kijelentkezése után hívódnak meg.

$user->onLoggedIn[] = function () {
	// a felhasználó éppen bejelentkezett
};

Identitás

Az identitás a felhasználóról szóló információk összessége, amelyet az authentikátor ad vissza, és amely ezt követően a sessionben tárolódik, és a $user->getIdentity() segítségével érhető el. Tehát lekérhetjük az azonosítót, a szerepköröket és egyéb felhasználói adatokat, ahogyan azokat az authentikátorban átadtuk:

$user->getIdentity()->getId();
// működik a $user->getId() rövidítés is;

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

// a felhasználói adatok property-ként érhetők el
// a név, amelyet a MyAuthenticatorban adtunk át
$user->getIdentity()->name;

Ami fontos, az az, hogy a $user->logout() segítségével történő kijelentkezéskor az identitás nem törlődik, és továbbra is rendelkezésre áll. Tehát bár a felhasználónak van identitása, nem feltétlenül van bejelentkezve. Ha explicit módon szeretnénk törölni az identitást, a logout(true) hívásával jelentkeztetjük ki a felhasználót.

Ennek köszönhetően továbbra is feltételezheti, hogy melyik felhasználó van a számítógépnél, és például személyre szabott ajánlatokat jeleníthet meg neki az e-shopban, de a személyes adatait csak a bejelentkezés után jelenítheti meg.

Az identitás egy objektum, amely implementálja a Nette\Security\IIdentity interfészt, az alapértelmezett implementáció a Nette\Security\SimpleIdentity. És ahogy említettük, a sessionben tárolódik, tehát ha például megváltoztatjuk valamelyik bejelentkezett felhasználó szerepkörét, a régi adatok az identitásában maradnak egészen az újbóli bejelentkezéséig.

A bejelentkezett felhasználó tárolója

A felhasználóról szóló két alapvető információ, tehát hogy be van-e jelentkezve és az ő identita, általában a sessionben kerül átvitelre. Ez megváltoztatható. Ezen információk tárolásáért egy objektum felel, amely implementálja a Nette\Security\UserStorage interfészt. Két standard implementáció áll rendelkezésre, az első a sessionben, a második a cookie-ban továbbítja az adatokat. Ezek a Nette\Bridges\SecurityHttp\SessionStorage és a CookieStorage osztályok. A tárolót kiválaszthatja és konfigurálhatja nagyon kényelmesen a security › authentication konfigurációban.

Továbbá befolyásolhatja, hogy pontosan hogyan történjen az identitás tárolása (sleep) és visszaállítása (wakeup). Elegendő, ha az authenticator implementálja a Nette\Security\IdentityHandler interfészt. Ennek két metódusa van: a sleepIdentity() az identitás tárolóba írása előtt hívódik meg, a wakeupIdentity() pedig annak kiolvasása után. A metódusok módosíthatják az identitás tartalmát, vagy helyettesíthetik egy új objektummal, amelyet visszaadnak. A wakeupIdentity() metódus akár null-t is visszaadhat, ezzel kijelentkeztetve a felhasználót.

Példaként megmutatjuk a gyakori kérdés megoldását, hogyan frissítsük a szerepköröket az identitásban rögtön a sessionből való betöltés után. A wakeupIdentity() metódusban átadjuk az identitásba az aktuális szerepköröket, pl. az adatbázisból:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// itt lehet módosítani az identitást a tárolóba írás előtt a bejelentkezés után,
		// de erre most nincs szükségünk
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// szerepkörök frissítése az identitásban
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

És most visszatérünk a cookie alapú tárolóhoz. Lehetővé teszi egy olyan weboldal létrehozását, ahol a felhasználók bejelentkezhetnek, és ehhez nincs szükség sessionökre. Tehát nincs szükség a lemezre írásra. Egyébként így működik az a weboldal is, amelyet éppen olvas, beleértve a fórumot is. Ebben az esetben az IdentityHandler implementálása elengedhetetlen. A cookie-ba ugyanis csak egy véletlenszerű tokent fogunk tárolni, amely a bejelentkezett felhasználót reprezentálja.

Először tehát a konfigurációban beállítjuk a kívánt tárolót a security › authentication › storage: cookie segítségével.

Az adatbázisban létrehozunk egy authtoken oszlopot, amelyben minden felhasználónak egy teljesen véletlenszerű, egyedi és kitalálhatatlan stringje lesz, megfelelő hosszúságú (legalább 13 karakter). A CookieStorage tároló a cookie-ban csak az $identity->getId() értékét továbbítja, tehát a sleepIdentity()-ben az eredeti identitást egy helyettesítő identitásra cseréljük, amelynek azonosítójában az authtoken van, míg a wakeupIdentity() metódusban az authtoken alapján kiolvassuk a teljes identitást az adatbázisból:

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);
		// ellenőrizzük a jelszót
		...
		// visszaadjuk az identitást az adatbázisból származó összes adattal
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// visszaadjuk a helyettesítő identitást, ahol az ID-ben az authtoken lesz
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// a helyettesítő identitást lecseréljük a teljes identitásra, mint az authenticate()-ban
		$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
		return $row
			? new SimpleIdentity($row->id, null, (array) $row)
			: null;
	}
}

Több független bejelentkezés

Egy weboldalon és egy sessionön belül párhuzamosan több független bejelentkező felhasználó is lehet. Ha például egy weboldalon külön authentikációt szeretnénk az adminisztrációhoz és a nyilvános részhez, elegendő mindegyiknek saját nevet beállítani:

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

Fontos megjegyezni, hogy a névteret mindig az adott részhez tartozó összes helyen be kell állítani. Ha presentereket használunk, a névteret az adott rész közös ősében állítjuk be – általában a BasePresenterben. Ezt a checkRequirements() metódus kiterjesztésével tesszük meg:

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

Több authentikátor

Az alkalmazás független bejelentkezéssel rendelkező részekre való felosztása általában különböző authentikátorokat is igényel. Azonban, ha a szolgáltatások konfigurációjában két, az Authenticator interfészt implementáló osztályt regisztrálnánk, a Nette nem tudná, melyiket rendelje automatikusan a Nette\Security\User objektumhoz, és hibát jelezne. Ezért az authentikátorokhoz az autowiring-ot úgy kell korlátoznunk, hogy csak akkor működjön, ha valaki egy konkrét osztályt kér, pl. FrontAuthenticator, amit az autowired: self opcióval érünk el:

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

A User objektum authentikátorát a login() metódus hívása előtt állítjuk be, tehát általában annak az űrlapnak a kódjában, amely bejelentkezteti:

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