Prijava uporabnikov (Avtentikacija)
Skoraj nobena spletna aplikacija se ne more izogniti mehanizmu prijave uporabnikov in preverjanja uporabniških dovoljenj. V tem poglavju bomo govorili o:
- prijava in odjava uporabnikov
- lastnih avtentikatorjih
V primerih bomo uporabljali objekt razreda Nette\Security\User, ki predstavlja trenutnega
uporabnika in do katerega pridete tako, da si ga pustite predati s pomočjo dependency injection. V presenterjih je dovolj samo
poklicati $user = $this->getUser()
.
Avtentikacija
Avtentikacija pomeni prijavo uporabnikov, torej proces, pri katerem se preverja, ali je uporabnik res tisti, za katerega
se izdaja. Običajno se dokazuje z uporabniškim imenom in geslom. Preverjanje izvede t.i. avtentikator. Če prijava ne uspe, se sproži
Nette\Security\AuthenticationException
.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Uporabniško ime ali geslo je napačno');
}
Na ta način uporabnika odjavite:
$user->logout();
In ugotavljanje, ali je prijavljen:
echo $user->isLoggedIn() ? 'da' : 'ne';
Zelo preprosto, kajne? In vse varnostne vidike rešuje Nette za vas.
V presenterjih lahko preverite prijavo v metodi startup()
in neprijavljenega uporabnika preusmerite na
prijavno stran.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Potek
Prijava uporabnika poteče skupaj s potekom shrambe, ki je običajno seja
(glej nastavitve poteka seje). Lahko pa nastavite tudi
krajši časovni interval, po preteku katerega pride do odjave uporabnika. Za to služi 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');
// preklic nastavljenega poteka
$user->setExpiration(null);
Ali je bil uporabnik odjavljen zaradi poteka časovnega intervala, razkrije metoda $user->getLogoutReason()
, ki
vrača bodisi konstanto Nette\Security\UserStorage::LogoutInactivity
(potekel časovni limit) ali
UserStorage::LogoutManual
(odjavljen z metodo logout()
).
Avtentikator
Gre za objekt, ki preverja prijavne podatke, torej praviloma ime in geslo. Trivialna oblika je razred Nette\Security\SimpleAuthenticator, ki ga lahko definiramo v konfiguraciji:
security:
users:
# ime: geslo
frantisek: tajnegeslo
katka: jestetajnejsigeslo
Ta rešitev je primerna bolj za testne namene. Pokazali si bomo, kako ustvariti avtentikator, ki bo preverjal prijavne podatke glede na podatkovno tabelo.
Avtentikator je objekt, ki implementira vmesnik Nette\Security\Authenticator z metodo
authenticate()
. Njegova naloga je bodisi vrniti t.i. identiteto ali sprožiti izjemo
Nette\Security\AuthenticationException
. Pri njej bi bilo mogoče še navesti kodo napake za natančnejše
razlikovanje nastale situacije: Authenticator::IdentityNotFound
in 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 polje več vlog
['name' => $row->username],
);
}
}
Razred MyAuthenticator komunicira s podatkovno bazo preko Nette Database
Explorer in dela s tabelo users
, kjer je v stolpcu username
prijavno ime uporabnika in v stolpcu
password
odtis gesla. Po preverjanju imena in gesla vrača
identiteto, ki nosi ID uporabnika, njegovo vlogo (stolpec role
v tabeli), o kateri si bomo več povedali kasneje, in polje z dodatnimi podatki (v našem primeru uporabniško ime).
Avtentikator še dodamo v konfiguracijo kot storitev DI vsebnika:
services:
- MyAuthenticator
Dogodki $onLoggedIn, $onLoggedOut
Objekt Nette\Security\User
ima dogodke
$onLoggedIn
in $onLoggedOut
, lahko torej dodate callbacke, ki se sprožijo po uspešni prijavi oz. po
odjavi uporabnika.
$user->onLoggedIn[] = function () {
// uporabnik je bil pravkar prijavljen
};
Identiteta
Identiteta predstavlja nabor informacij o uporabniku, ki jih vrača avtentikator in ki se nato shranjujejo v seji ter jih
pridobivamo s pomočjo $user->getIdentity()
. Lahko torej pridobimo id, vloge in druge uporabniške podatke, tako
kot smo si jih predali v avtentikatorju:
$user->getIdentity()->getId();
// deluje tudi bližnjica $user->getId();
$user->getIdentity()->getRoles();
// uporabniški podatki so dostopni kot lastnosti
// ime, ki smo si ga predali v MyAuthenticator
$user->getIdentity()->name;
Pomembno je, da se pri odjavi s pomočjo $user->logout()
identiteta ne izbriše in je še naprej na
voljo. Torej, čeprav ima uporabnik identiteto, ni nujno prijavljen. Če bi želeli identiteto eksplicitno izbrisati, odjavimo
uporabnika s klicem logout(true)
.
Zahvaljujoč temu lahko še naprej predvidevate, kateri uporabnik je za računalnikom in mu na primer v e-trgovini prikazujete personalizirane ponudbe, vendar mu njegove osebne podatke lahko prikažete šele po prijavi.
Identiteta je objekt, ki implementira vmesnik Nette\Security\IIdentity, privzeta implementacija je Nette\Security\SimpleIdentity. In kot je bilo omenjeno, se vzdržuje v seji, zato če na primer spremenimo vlogo katerega od prijavljenih uporabnikov, ostanejo stari podatki v njegovi identiteti vse do njegove ponovne prijave.
Shramba prijavljenega uporabnika
Dve osnovni informaciji o uporabniku, torej ali je prijavljen in njegova identita, se praviloma
prenašata v seji. Kar pa je mogoče spremeniti. Za shranjevanje teh informacij skrbi objekt, ki implementira vmesnik
Nette\Security\UserStorage
. Na voljo sta dve standardni implementaciji, prva prenaša podatke v seji in druga
v piškotku. Gre za razreda Nette\Bridges\SecurityHttp\SessionStorage
in CookieStorage
. Izbrati si
shrambo in jo konfigurirati lahko zelo udobno v konfiguraciji security › authentication.
Nadalje lahko vplivate na to, kako natančno bo potekalo shranjevanje identitete (sleep) in obnavljanje (wakeup).
Dovolj je, da avtentikator implementira vmesnik Nette\Security\IdentityHandler
. Ta ima dve metodi:
sleepIdentity()
se kliče pred zapisom identitete v shrambo in wakeupIdentity()
po njenem prebranju.
Metodi lahko vsebino identitete spremenita, ali pa jo nadomestita z novim objektom, ki ga vrneta. Metoda
wakeupIdentity()
lahko celo vrne null
, s čimer uporabnika odjavi.
Kot primer si bomo pokazali rešitev pogostega vprašanja, kako posodobiti vloge v identiteti takoj po nalaganju iz seje.
V metodi wakeupIdentity()
predamo v identiteto trenutne vloge npr. iz podatkovne baze:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// tukaj je mogoče spremeniti identiteto pred zapisom v shrambo po prijavi,
// vendar tega zdaj ne potrebujemo
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// posodobitev vlog v identiteti
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
In zdaj se vrnemo k shrambi na osnovi piškotkov. Dovoljuje vam ustvariti spletno stran, kjer se lahko prijavljajo uporabniki,
ne da bi potrebovali seje. Torej ni treba zapisovati na disk. Navsezadnje tako deluje tudi spletna stran, ki jo pravkar berete,
vključno s forumom. V tem primeru je implementacija IdentityHandler
nujna. V piškotek bomo namreč shranjevali
samo naključni žeton, ki predstavlja prijavljenega uporabnika.
Najprej torej v konfiguraciji nastavimo zahtevano shrambo s pomočjo
security › authentication › storage: cookie
.
V podatkovni bazi si ustvarimo stolpec authtoken
, v katerem bo imel vsak uporabnik popolnoma naključen, unikaten in neugotovljiv niz zadostne dolžine (vsaj
13 znakov). Shramba CookieStorage
prenaša v piškotku samo vrednost $identity->getId()
, zato v
sleepIdentity()
originalno identiteto nadomestimo z nadomestno z authtoken
v ID, nasprotno pa
v metodi wakeupIdentity()
glede na authtoken preberemo celotno identiteto iz podatkovne baze:
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);
// preverimo geslo
...
// vrnemo identiteto z vsemi podatki iz podatkovne baze
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// vrnemo nadomestno identiteto, kjer bo v ID authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// nadomestno identiteto nadomestimo s polno identiteto, kot v authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
}
Več neodvisnih prijav
Hkrati je mogoče v okviru ene spletne strani in ene seje imeti več neodvisnih prijavljenih uporabnikov. Če na primer želimo imeti na spletni strani ločeno avtentikacijo za administracijo in javni del, je dovolj vsaki od njih nastaviti lastno ime:
$user->getStorage()->setNamespace('backend');
Pomembno je vedeti, da moramo imenski prostor nastaviti vedno na vseh mestih, ki pripadajo danemu delu. Če uporabljamo presenterje, nastavimo imenski prostor v skupnem predniku za dani del – običajno BasePresenter. To storimo z razširitvijo metode checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Več avtentikatorjev
Razdelitev aplikacije na dele z neodvisno prijavo večinoma zahteva tudi različne avtentikatorje. Takoj ko pa bi
v konfiguraciji storitev registrirali dva razreda, ki implementirata Authenticator, Nette ne bi vedelo, katerega od njiju
samodejno dodeliti objektu Nette\Security\User
, in bi prikazalo napako. Zato moramo za avtentikatorje autowiring omejiti tako, da deluje, samo ko nekdo zahteva
konkreten razred, npr. FrontAuthenticator, kar dosežemo z izbiro autowired: self
:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Avtentikator objekta User nastavimo pred klicem metode login(), torej običajno v kodi obrazca, ki ga prijavlja:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};