Autentificarea utilizatorilor
Aplicațiile web mici sau deloc nu au nevoie de un mecanism de autentificare a utilizatorilor sau de verificare a privilegiilor acestora. În acest capitol, vom vorbi despre:
- autentificarea și deconectarea utilizatorului
- autentificatori și autorizatori personalizați
În exemple, vom folosi un obiect din clasa Nette\Security\User, care reprezintă utilizatorul
curent și pe care îl obținem prin trecerea acestuia cu ajutorul injecției de dependență. În prezentatori, este
suficient să apelați $user = $this->getUser()
.
Autentificare
Autentificarea înseamnă conectarea utilizatorului, adică procesul în timpul căruia este verificată identitatea
unui utilizator. De obicei, utilizatorul se identifică folosind numele de utilizator și parola. Verificarea este efectuată de
așa-numitul autentificator. În cazul în care autentificarea eșuează, se aruncă
Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('The username or password you entered is incorrect.');
}
Iată cum se deconectează utilizatorul:
$user->logout();
Și verificarea dacă utilizatorul este logat:
echo $user->isLoggedIn() ? 'yes' : 'no';
Simplu, nu? Și toate aspectele legate de securitate sunt gestionate de Nette pentru dumneavoastră.
În presenter, puteți verifica autentificarea în metoda startup()
și puteți redirecționa un utilizator care
nu s-a autentificat 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 depozitului, care este de
obicei o sesiune (a se vedea setarea de expirare a sesiunii
). Cu toate acestea, puteți seta, de asemenea, un interval de timp mai scurt după care utilizatorul este deconectat. În acest
scop, se utilizează metoda setExpiration()
, care este apelată înainte de login()
. Furnizați ca
parametru un șir de caractere cu un timp relativ:
// autentificarea expiră după 30 de minute de inactivitate
$user->setExpiration('30 minutes');
// anulați setul de expirare
$user->setExpiration(null);
Metoda $user->getLogoutReason()
spune dacă utilizatorul a fost deconectat deoarece intervalul de timp a
expirat. Aceasta returnează fie constanta Nette\Security\UserStorage::LogoutInactivity
dacă timpul a expirat, fie
UserStorage::LogoutManual
atunci când a fost apelată metoda logout()
.
Autentificator
Este un obiect care verifică datele de autentificare, adică, de obicei, numele și parola. Implementarea trivială este clasa Nette\Security\SimpleAuthenticator, care poate fi definită în configurare:
security:
users:
# nume: parola
johndoe: secret123
kathy: evenmoresecretpassword
Această soluție este mai potrivită pentru scopuri de testare. Vă vom arăta cum să creați un autentificator care va verifica acreditările în raport cu un tabel de bază de date.
Un autentificator este un obiect care implementează interfața Nette\Security\Authenticator cu metoda
authenticate()
. Sarcina sa este fie de a returna așa-numita identitate, fie de a arunca
o excepție Nette\Security\AuthenticationException
. De asemenea, ar fi posibil să se furnizeze un cod de eroare de
grad fin Authenticator::IdentityNotFound
sau 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 o matrice de roluri
['name' => $row->username],
);
}
}
Clasa MyAuthenticator comunică cu baza de date prin intermediul Nette
Database Explorer și lucrează cu tabelul users
, în care coloana username
conține numele de
autentificare al utilizatorului, iar coloana password
conține hash-ul. După ce verifică numele și parola, aceasta returnează
identitatea cu ID-ul utilizatorului, rolul (coloana role
din tabel), pe care îl vom menționa mai târziu, și un array cu date suplimentare (în cazul nostru, numele de utilizator).
Vom adăuga autentificatorul la configurație ca serviciu al containerului DI:
services:
- MyAuthenticator
Evenimentele $onLoggedIn, $onLoggedOut
Obiectul Nette\Security\User
are evenimentele
$onLoggedIn
și $onLoggedOut
, astfel încât puteți adăuga callback-uri care sunt declanșate după
o autentificare reușită sau după ce utilizatorul se deconectează.
$user->onLoggedIn[] = function () {
// utilizatorul tocmai s-a logat
};
Identitate
O identitate este un set de informații despre un utilizator care este returnat de către autentificator și care este apoi
stocat într-o sesiune și recuperat folosind $user->getIdentity()
. Astfel, putem obține id-ul, rolurile și
alte date despre utilizator așa cum le-am transmis în autentificator:
$user->getIdentity()->getId();
// funcționează și prescurtarea $user->getId();
$user->getIdentity()->getRoles();
// datele utilizatorului pot fi accesate ca proprietăți
// numele pe care l-am transmis în MyAuthenticator
$user->getIdentity()->name;
Este important de menționat că, atunci când utilizatorul se deconectează folosind $user->logout()
,
identitatea nu este ștearsă și este încă disponibilă. Deci, dacă identitatea există, aceasta nu garantează prin ea
însăși că utilizatorul este, de asemenea, conectat. Dacă dorim să ștergem în mod explicit identitatea, trebuie să
deconectăm utilizatorul prin logout(true)
.
Datorită acestui lucru, puteți presupune în continuare ce utilizator se află la calculator și, de exemplu, puteți afișa oferte personalizate în magazinul electronic, însă puteți afișa datele sale personale numai după ce s-a logat.
Identity este un obiect care implementează interfața Nette\Security\IIdentity, implementarea implicită fiind Nette\Security\SimpleIdentity. Și, după cum am menționat, identitatea este stocată în sesiune, astfel încât, dacă, de exemplu, schimbăm rolul unora dintre utilizatorii conectați, datele vechi vor fi păstrate în identitate până când acesta se conectează din nou.
Stocarea pentru utilizatorul conectat
Cele două informații de bază despre utilizator, și anume dacă este conectat și identitatea
sa, sunt de obicei stocate în sesiune. Care pot fi modificate. Pentru stocarea acestor informații este responsabil un obiect
care implementează interfața Nette\Security\UserStorage
. Există două implementări standard, prima transmite
datele într-o sesiune, iar cea de-a doua într-un cookie. Acestea sunt clasele
Nette\Bridges\SecurityHttp\SessionStorage
și CookieStorage
. Puteți alege stocarea și o puteți
configura foarte convenabil în configurația securitate ›
autentificare.
De asemenea, puteți controla exact modul în care va avea loc salvarea (sleep) și restaurarea (wakeup)
identității. Tot ce trebuie să faceți este ca autentificatorul să implementeze interfața
Nette\Security\IdentityHandler
. Aceasta are două metode: sleepIdentity()
este apelată înainte ca
identitatea să fie scrisă în memorie, iar wakeupIdentity()
este apelată după ce identitatea este citită.
Metodele pot modifica conținutul identității sau o pot înlocui cu un nou obiect care se întoarce. Metoda
wakeupIdentity()
poate chiar să returneze null
, care deconectează utilizatorul.
Ca exemplu, vom prezenta o soluție la o întrebare frecventă privind modul de actualizare a rolurilor identității imediat
după restaurarea dintr-o sesiune. În metoda wakeupIdentity()
transmitem rolurile curente către identitate, de
exemplu, din baza de date:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// aici puteți schimba identitatea înainte de a o stoca după autentificare,
// dar nu avem nevoie de asta acum
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. Acesta vă permite să creați un site web în care utilizatorii se
pot autentifica fără a fi nevoie să folosiți sesiuni. Deci nu este nevoie să scrie pe disc. La urma urmei, acesta este modul
în care funcționează site-ul web pe care îl citiți acum, inclusiv forumul. În acest caz, implementarea
IdentityHandler
este o necesitate. Vom stoca în cookie doar un token aleatoriu reprezentând
utilizatorul logat.
Așadar, mai întâi setăm stocarea dorită în configurație folosind
security › authentication › storage: cookie
.
Vom adăuga o coloană authtoken
în baza de date, în care fiecare utilizator va avea un șir complet aleatoriu, unic și imposibil de ghicit, de lungime suficientă (cel
puțin 13 caractere). Depozitul CookieStorage
stochează doar valoarea $identity->getId()
în
cookie, astfel că în sleepIdentity()
înlocuim identitatea originală cu un proxy cu authtoken
în ID,
în schimb în metoda wakeupIdentity()
restaurăm întreaga identitate din baza de date conform 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ți 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 proxy, în care ID-ul este authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// înlocuim identitatea proxy cu o identitate 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;
}
}
Autentificări independente multiple
Este posibil să existe mai mulți utilizatori autentificați independent în cadrul unui site și a unei sesiuni la un moment dat. De exemplu, dacă dorim să avem o autentificare separată pentru frontend și backend, vom seta doar un spațiu de nume de sesiune unic pentru fiecare dintre ele:
$user->getStorage()->setNamespace('backend');
Este necesar să reținem că acesta trebuie setat în toate locurile care aparțin aceluiași segment. Atunci când folosim prezentatori, vom seta spațiul de nume în strămoșul comun – de obicei BasePresenter. Pentru a face acest lucru, vom extinde metoda checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Autentificatori multipli
Împărțirea unei aplicații în segmente cu autentificare independentă necesită, în general, autentificatori diferiți. Cu
toate acestea, înregistrarea a două clase care implementează Authenticator în serviciile de configurare ar declanșa
o eroare, deoarece Nette nu ar ști care dintre ele ar trebui să fie conectată automat la obiectul
Nette\Security\User
. De aceea, trebuie să limităm autowiring-ul pentru acestea cu autowired: self
,
astfel încât să fie activat numai atunci când clasa lor este solicitată în mod specific:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Trebuie doar să ne setăm autentificatorul la obiectul User înainte de a apela metoda login(), ceea ce înseamnă, de obicei, în callback-ul formularului de autentificare:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};