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

Namestitev in zahteve

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);
	// ...
};
različica: 4.0