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