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