Preverjanje pristnosti uporabnikov
Malo ali nič spletne aplikacije ne potrebujejo mehanizma za prijavo uporabnika ali preverjanje njegovih pravic. V tem poglavju bomo govorili o:
- prijavi in odjavi uporabnika
- lastnih avtentifikatorjih in avtorizatorjih
V primerih bomo uporabili objekt razreda Nette\Security\User, ki predstavlja trenutnega
uporabnika in ga dobite s posredovanjem z uporabo vbrizgavanja odvisnosti. V predstavitvah preprosto
pokličite $user = $this->getUser()
.
Preverjanje pristnosti
Avtentikacija pomeni prijavo uporabnika, tj. postopek, med katerim se preveri identiteta uporabnika. Uporabnik se
običajno identificira z uporabniškim imenom in geslom. Preverjanje opravi tako imenovani avtentifikator. Če prijava ni uspešna, se vrže
Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('The username or password you entered is incorrect.');
}
Tako se uporabnik odjavi:
$user->logout();
In preverjanje, ali je uporabnik prijavljen:
echo $user->isLoggedIn() ? 'yes' : 'no';
Preprosto, kajne? Za vse varnostne vidike poskrbi Nette.
V programu Presenter lahko v metodi startup()
preverite prijavo in neprijavljenega uporabnika preusmerite na
prijavno stran.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Iztek veljavnosti
Prijava uporabnika poteče skupaj s potekom veljavnosti shrambe, ki je običajno
seja (glejte nastavitev poteka seje ). Lahko pa nastavite
tudi krajši časovni interval, po katerem se uporabnik odjavi. V ta namen se uporablja metoda setExpiration()
, ki
se kliče pred login()
. Kot parameter navedite niz z relativnim časom:
// prijava poteče po 30 minutah neaktivnosti
$user->setExpiration('30 minutes');
// preklicati nastavitev izteka veljavnosti
$user->setExpiration(null);
Metoda $user->getLogoutReason()
pove, ali je bil uporabnik odjavljen, ker se je časovni interval iztekel. Vrne
bodisi konstanto Nette\Security\UserStorage::LogoutInactivity
, če se je čas iztekel, bodisi
UserStorage::LogoutManual
, če je bila klicana metoda logout()
.
Avtentifikator
To je objekt, ki preveri podatke za prijavo, tj. običajno ime in geslo. Trivialna izvedba je razred Nette\Security\SimpleAuthenticator, ki ga je mogoče opredeliti v konfiguraciji:
security:
users:
# ime: geslo
johndoe: secret123
kathy: evenmoresecretpassword
Ta rešitev je primernejša za namene testiranja. Pokazali vam bomo, kako ustvariti avtentifikator, ki bo preverjal poverilnice glede na tabelo podatkovne zbirke.
Avtentifikator je objekt, ki implementira vmesnik Nette\Security\Authenticator z metodo
authenticate()
. Njegova naloga je, da vrne tako imenovano identiteto ali pa vrže izjemo
Nette\Security\AuthenticationException
. Prav tako bi bilo mogoče zagotoviti drobno kodo napake
Authenticator::IdentityNotFound
ali 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, // ali niz vlog
['name' => $row->username],
);
}
}
Razred MyAuthenticator komunicira s podatkovno zbirko prek Nette Database
Explorerja in dela s tabelo users
, kjer stolpec username
vsebuje uporabniško prijavno ime, stolpec
password
pa hash. Po preverjanju imena in gesla vrne
identiteto z ID uporabnika, vlogo (stolpec role
v tabeli), ki jo bomo omenili kasneje, in
polje z dodatnimi podatki (v našem primeru uporabniško ime).
Avtentifikator bomo dodali v konfiguracijo kot storitev vsebnika DI:
services:
- MyAuthenticator
$onLoggedIn, $onLoggedOut Dogodki
Objekt Nette\Security\User
ima dogodka
$onLoggedIn
in $onLoggedOut
, zato lahko dodate povratne klice, ki se sprožijo po uspešni prijavi ali
po odjavi uporabnika.
$user->onLoggedIn[] = function () {
// uporabnik se je pravkar prijavil
};
Identiteta
Identiteta je niz informacij o uporabniku, ki jih vrne avtentifikator in se nato shranijo v sejo ter prikličejo z uporabo
$user->getIdentity()
. Tako lahko dobimo id, vloge in druge podatke o uporabniku, kot smo jih posredovali
v avtentifikatorju:
$user->getIdentity()->getId();
// deluje tudi bližnjica $user->getId();
$user->getIdentity()->getRoles();
// do podatkov o uporabniku lahko dostopate kot do lastnosti
// ime, ki smo ga posredovali v MyAuthenticator
$user->getIdentity()->name;
Pomembno je, da ko se uporabnik odjavi z uporabo $user->logout()
, identiteta ni izbrisana in je še
vedno na voljo. Torej, če identiteta obstaja, sama po sebi ne zagotavlja, da je uporabnik tudi prijavljen. Če želimo identiteto
izrecno izbrisati, uporabnika odjavimo s logout(true)
.
Zaradi tega lahko še vedno predvidevamo, kateri uporabnik je v računalniku, in na primer v e-trgovini prikažemo prilagojene ponudbe, vendar lahko njegove osebne podatke prikažemo šele po prijavi.
Identiteta je objekt, ki implementira vmesnik Nette\Security\IIdentity, privzeta implementacija je Nette\Security\SimpleIdentity. In kot smo že omenili, se identiteta hrani v seji, zato če na primer spremenimo vlogo katerega od prijavljenih uporabnikov, bodo stari podatki ostali v identiteti, dokler se ta ponovno ne prijavi.
Shranjevanje za prijavljenega uporabnika
Dve osnovni informaciji o uporabniku, tj. ali je prijavljen in njegova identiteta, sta običajno
shranjeni v seji. Ki jih je mogoče spremeniti. Za shranjevanje teh informacij je odgovoren objekt, ki implementira vmesnik
Nette\Security\UserStorage
. Obstajata dve standardni izvedbi, prva prenaša podatke v seji, druga pa v piškotku.
To sta razreda Nette\Bridges\SecurityHttp\SessionStorage
in CookieStorage
. Shranjevanje lahko izberete
in konfigurirate zelo priročno v konfiguraciji varnost ›
avtentikacija.
Prav tako lahko natančno nadzirate, kako bo potekalo shranjevanje identitete (sleep) in obnavljanje (wakeup).
Vse, kar potrebujete, je, da avtentifikator implementira vmesnik Nette\Security\IdentityHandler
. Ta ima dve metodi:
sleepIdentity()
se pokliče, preden se identiteta zapiše v pomnilnik, wakeupIdentity()
pa se pokliče,
ko se identiteta prebere. Metodi lahko spremenita vsebino identitete ali jo nadomestita z novim objektom, ki se vrne. Metoda
wakeupIdentity()
lahko vrne celo null
, ki uporabnika odjavi.
Kot primer bomo prikazali rešitev pogostega vprašanja, kako posodobiti identitetne vloge takoj po obnovitvi iz seje.
V metodi wakeupIdentity()
identiteti posredujemo trenutne vloge, npr. iz zbirke podatkov:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// tukaj lahko spremenite identiteto pred shranjevanjem po prijavi,
// vendar tega zdaj ne potrebujemo.
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// posodabljanje vlog v identiteti
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
Zdaj se vrnemo k shranjevanju na podlagi piškotkov. Z njo lahko ustvarite spletno mesto, na katerem se lahko uporabniki
prijavijo, ne da bi jim bilo treba uporabljati seje. Torej ni treba pisati na disk. Navsezadnje tako deluje spletno mesto, ki ga
zdaj berete, vključno s forumom. V tem primeru je implementacija spletne strani IdentityHandler
nujna.
V piškotek bomo shranili le naključni žeton, ki predstavlja prijavljenega uporabnika.
Zato najprej v konfiguraciji nastavimo želeno shranjevanje z uporabo
security › authentication › storage: cookie
.
V zbirko podatkov bomo dodali stolpec authtoken
, v katerem bo imel vsak uporabnik popolnoma naključen, edinstven in nezgrešljiv niz zadostne dolžine (vsaj
13 znakov). V shrambi CookieStorage
je v piškotku shranjena le vrednost $identity->getId()
, zato
v metodi sleepIdentity()
prvotno identiteto nadomestimo s posrednikom z authtoken
v ID, nasprotno pa
v metodi wakeupIdentity()
obnovimo celotno identiteto iz zbirke podatkov v skladu z authtokenom:
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);
// preverjanje gesla
...
// vrnemo identiteto z vsemi podatki iz zbirke podatkov
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// vrnemo proxy identiteto, kjer je ID avtotoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// nadomestimo proxy identiteto s polno identiteto, kot v funkciji authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
}
Več neodvisnih avtentikacij
Na enem spletnem mestu in v eni seji je mogoče imeti več neodvisno prijavljenih uporabnikov. Če na primer želimo imeti ločeno avtentikacijo za frontend in backend, bomo za vsakega od njiju samo nastavili edinstven imenski prostor seje:
$user->getStorage()->setNamespace('backend');
Upoštevati je treba, da je to treba nastaviti na vseh mestih, ki pripadajo istemu segmentu. Pri uporabi predstavnikov bomo imenski prostor nastavili v skupnem predniku – običajno je to BasePresenter. V ta namen bomo razširili metodo checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Več overiteljev
Delitev aplikacije na segmente z neodvisnim preverjanjem pristnosti običajno zahteva različne overitelje. Vendar pa bi
registracija dveh razredov, ki implementirata avtentifikator, v konfiguracijske storitve sprožila napako, ker Nette ne bi vedel,
kateri od njiju naj bo samodejno povezan z objektom
Nette\Security\User
. Zato moramo z objektom autowired: self
omejiti avtentikacijo zanju, tako da se
aktivira le, kadar je njun razred posebej zahtevan:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Avtentifikator moramo nastaviti objektu User le pred klicem metode login(), kar običajno pomeni v povratnem klicu obrazca za prijavo:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};