Felhasználók hitelesítése

A kevés vagy semmilyen webes alkalmazásoknak nincs szükségük a felhasználói bejelentkezéshez vagy a felhasználói jogosultságok ellenőrzéséhez szükséges mechanizmusra. Ebben a fejezetben a következőkről fogunk beszélni:

  • felhasználói bejelentkezés és kijelentkezés
  • egyéni hitelesítők és engedélyezők

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

A példákban egy Nette\Security\User osztályú objektumot fogunk használni, amely az aktuális felhasználót képviseli, és amelyet függőségi injektálással átadva kapunk meg. A prezenterekben egyszerűen hívjuk meg a $user = $this->getUser().

Hitelesítés

A hitelesítés a felhasználó bejelentkezését jelenti, azaz azt a folyamatot, amelynek során a felhasználó személyazonosságát ellenőrzik. A felhasználó általában felhasználónév és jelszó segítségével azonosítja magát. Az ellenőrzést az úgynevezett hitelesítő végzi. Ha a bejelentkezés sikertelen, akkor a Nette\Security\AuthenticationException.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('The username or password you entered is incorrect.');
}

Így jelentkezik ki a felhasználó:

$user->logout();

És ellenőrzi, hogy a felhasználó be van-e jelentkezve:

echo $user->isLoggedIn() ? 'yes' : 'no';

Egyszerű, igaz? És minden biztonsági szempontot a Nette kezel az Ön számára.

A prezenterben a startup() módszerrel ellenőrizheti a bejelentkezést, é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ói bejelentkezés a tároló lejáratával együtt jár le, ami általában egy munkamenet (lásd a munkamenet lejárati beállítását). Beállíthat azonban rövidebb időintervallumot is, amely után a felhasználó kijelentkezik. Erre a célra a setExpiration() metódus szolgál, amelyet a login() előtt kell meghívni. Paraméterként egy relatív időt tartalmazó karakterláncot adjon meg:

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

// törli a beállított lejáratot
$user->setExpiration(null);

A $user->getLogoutReason() metódus megmondja, hogy a felhasználó kijelentkezett-e, mert az időintervallum lejárt. Vagy a Nette\Security\UserStorage::LogoutInactivity konstans értéket adja vissza, ha az idő lejárt, vagy a UserStorage::LogoutManual értéket, ha a logout() metódust hívta meg.

Hitelesítő

Ez egy olyan objektum, amely ellenőrzi a bejelentkezési adatokat, azaz általában a nevet és a jelszót. A triviális megvalósítás a Nette\Security\SimpleAuthenticator osztály, amely a konfigurációban definiálható:

security:
	users:
		# name: password
		johndoe: secret123
		kathy: evenmoresecretpassword

Ez a megoldás inkább tesztelési célokra alkalmas. Megmutatjuk, hogyan hozzunk létre egy olyan hitelesítő eszközt, amely egy adatbázis-táblával szemben ellenőrzi a hitelesítő adatokat.

A hitelesítő egy olyan objektum, amely a Nette\Security\Authenticator interfészt valósítja meg a authenticate() metódussal. Feladata vagy az úgynevezett identitás visszaadása, vagy egy kivétel dobása Nette\Security\AuthenticationException. Lehetséges lenne egy finomhangolt hibakódot is megadni Authenticator::IdentityNotFound vagy 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('Felhasználó nem található.');
		}

		if (!$this->passwords->verify($password, $row->password)) {
			throw new Nette\Security\AuthenticationException('Érvénytelen jelszó.');
		}

		return new SimpleIdentity(
			$row->id,
			$row->role, // vagy szerepek tömbje.
			['name' => $row->username],
		);
	}
}

A MyAuthenticator osztály a Nette Database Explorer-en keresztül kommunikál az adatbázissal, és a users táblával dolgozik, ahol a username oszlop a felhasználó bejelentkezési nevét, a password oszlop pedig a hash-t tartalmazza. A név és a jelszó ellenőrzése után visszaadja az identitást a felhasználó azonosítójával, szerepével (a táblázat role oszlopa), amelyet később megemlítünk, és egy további adatokat (esetünkben a felhasználónevet) tartalmazó tömböt.

A hitelesítőt a DI konténer szolgáltatásaként adjuk hozzá a konfigurációhoz:

services:
	- MyAuthenticator

$onLoggedIn, $onLoggedOut Események

A Nette\Security\User objektum rendelkezik $onLoggedIn és $onLoggedOuteseményekkel, így olyan visszahívásokat adhat hozzá, amelyek a sikeres bejelentkezés vagy a felhasználó kijelentkezése után lépnek működésbe.

$user->onLoggedIn[] = function () {
	// a felhasználó épp most lépett be
};

Identitás

Az identitás a felhasználóra vonatkozó információk összessége, amelyet a hitelesítők visszaküldenek, majd egy munkamenetben tárolnak, és a $user->getIdentity() segítségével lekérdezhetők. Így megkaphatjuk az azonosítót, a szerepeket és más felhasználói adatokat, ahogyan azokat az autentikátorban átadtuk:

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

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

// a felhasználói adatokhoz tulajdonságokként is hozzáférhetünk
// a MyAuthenticatorban átadott név
$user->getIdentity()->name;

Fontos, hogy amikor a felhasználó a $user->logout() segítségével kijelentkezik, az azonosság nem törlődik, és továbbra is elérhető. Tehát, ha az identitás létezik, az önmagában nem biztosítja, hogy a felhasználó be is van jelentkezve. Ha kifejezetten törölni akarjuk az identitást, akkor a logout(true) segítségével jelentkezünk ki a felhasználóból.

Ennek köszönhetően továbbra is feltételezhetjük, hogy melyik felhasználó van a számítógépen, és például személyre szabott ajánlatokat jeleníthetünk meg a webáruházban, azonban személyes adatait csak bejelentkezés után jeleníthetjük meg.

Az Identity egy olyan objektum, amely megvalósítja a Nette\Security\IIdentity interfészt, az alapértelmezett megvalósítás a Nette\Security\SimpleIdentity. És mint említettük, az identitás a munkamenetben tárolódik, így ha például megváltoztatjuk valamelyik bejelentkezett felhasználó szerepét, a régi adatok az identitásban megmaradnak, amíg újra be nem jelentkezik.

A bejelentkezett felhasználó tárolása

A felhasználóra vonatkozó két alapvető információt, vagyis azt, hogy bejelentkezett-e, és a személyazonosságát általában a munkamenet hordozza. Amelyek megváltoztathatók. Ezen információk tárolásáért egy, a Nette\Security\UserStorage interfészt megvalósító objektum felelős. Két szabványos megvalósítás létezik, az első a munkamenetben, 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ást kiválaszthatja és nagyon kényelmesen konfigurálhatja a konfigurációban security › authentication.

Azt is pontosan szabályozhatjuk, hogy az identitás mentése (sleep) és visszaállítása (wakeup) hogyan történjen. Mindössze arra van szükség, hogy a hitelesítő 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, a wakeupIdentity() pedig az identitás beolvasása után hívódik. A metódusok módosíthatják az identitás tartalmát, vagy egy új objektummal helyettesíthetik azt, amely visszatér. A wakeupIdentity() metódus akár a null metódust is visszaadhatja, amely kijelentkezik a felhasználóból.

Példaként egy gyakori kérdésre mutatunk megoldást, hogy hogyan lehet az identitás szerepeket frissíteni közvetlenül a munkamenetből való visszaállítás után. A wakeupIdentity() metódusban átadjuk az aktuális szerepeket az identitásnak, pl. az adatbázisból:

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

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// szerepek 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áshoz. Ez lehetővé teszi egy olyan weboldal létrehozását, ahol a felhasználók bejelentkezhetnek anélkül, hogy munkameneteket kellene használniuk. Tehát nem kell a lemezre írni. Végül is így működik a weboldal, amit most olvasol, beleértve a fórumot is. Ebben az esetben a IdentityHandler megvalósítása szükségszerű. A cookie-ban csak egy véletlenszerű tokent fogunk tárolni, amely a bejelentkezett felhasználót képviseli.

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

Hozzáadunk egy authtoken oszlopot az adatbázisban, amelyben minden felhasználóhoz egy teljesen véletlenszerű, egyedi és megfejthetetlen, megfelelő hosszúságú (legalább 13 karakteres) karakterláncot fogunk létrehozni. A CookieStorage tároló csak a $identity->getId() értéket tárolja a cookie-ban, így a sleepIdentity() -ben az eredeti azonosítót egy proxyval helyettesítjük a authtoken azonosítóval, ezzel szemben a wakeupIdentity() módszerben az authtoken szerint a teljes azonosítót visszaállítjuk 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);
		// jelszó ellenőrzése
		...
		// visszaküldjük 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
	{
		// egy proxy identitást adunk vissza, ahol az azonosító az authtoken.
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// a proxy azonosítót a teljes azonosítóval helyettesítjük, mint az authenticate() esetében.
		$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
		return $row
			? new SimpleIdentity($row->id, null, (array) $row)
			: null;
	}
}

Többszörös független hitelesítés

Lehetőség van arra, hogy egy webhelyen belül és egy munkamenetben több független bejelentkezett felhasználó legyen. Ha például külön hitelesítést szeretnénk a frontend és a backend számára, akkor csak beállítunk egy-egy egyedi munkamenet névteret mindkettőhöz:

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

Szükséges szem előtt tartani, hogy ezt minden, ugyanahhoz a szegmenshez tartozó helyen be kell állítani. Előadók használata esetén a névteret a közös ősben – általában a BasePresenterben – állítjuk be. Ehhez a checkRequirements() metódust fogjuk kiterjeszteni:

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

Több hitelesítő

Egy alkalmazás független hitelesítéssel rendelkező szegmensekre való felosztása általában különböző hitelesítőket igényel. Azonban két Authenticator-t megvalósító osztály regisztrálása a konfigurációs szolgáltatásokba hibát váltana ki, mivel a Nette nem tudná, hogy melyiküknek kell automatikusan bekötni a Nette\Security\User objektumot. Ezért kell korlátozni az autowiringet számukra a autowired: self segítségével, hogy csak akkor aktiválódjon, ha az osztályukat kifejezetten kérik:

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

Csak a login() metódus hívása előtt kell beállítanunk a hitelesítőnket a User objektumra, ami jellemzően a bejelentkezési űrlap visszahívását jelenti:

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