Přihlašování uživatelů (Autentizace)
Pomalu žádná webová aplikace se neobejde bez mechanismu přihlašování uživatelů a ověřování uživatelských oprávnění. V této kapitole si povíme o:
- přihlašování a odhlašování uživatelů
- vlastních autentikátorech
V příkladech budeme používat objekt třídy Nette\Security\User, který představuje aktuálního
uživatele a ke kterému se dostanete tak, že si jej necháte předat pomocí dependency injection. V presenterech stačí jen
zavolat $user = $this->getUser().
Autentizace
Autentizací se rozumí přihlašování uživatelů, tedy proces, při kterém se ověřuje, zda je uživatel opravdu
tím, za koho se vydává. Obvykle se prokazuje uživatelským jménem a heslem. Ověření provede tzv. autentikátor. Pokud přihlášení selže, vyhodí se
Nette\Security\AuthenticationException.
try {
$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
$this->flashMessage('Uživatelské jméno nebo heslo je nesprávné');
}
Tímto způsobem uživatele odhlásíte:
$user->logout();
A zjištění, že je přihlášen:
echo $user->isLoggedIn() ? 'ano' : 'ne';
Velmi jednoduché, viďte? A všechny bezpečnostní aspekty řeší Nette za vás.
V presenterech můžete ověřit přihlášení v metodě startup() a nepřihlášeného uživatele
přesměrovat na přihlašovací stránku.
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
Expirace
Přihlášení uživatele expiruje společně s expirací úložiště,
kterým je obvykle session (viz nastavení expirace
session). Lze ale nastavit i kratší časový interval, po jehož uplynutí dojde k odhlášení uživatele. K tomu
slouží metoda setExpiration(), která se volá před login(). Jako parametr uveďte řetězec
s relativním časem:
// přihlášení vyprší po 30 minutách neaktivity
$user->setExpiration('30 minutes');
// zrušení nastavené expirace
$user->setExpiration(null);
Jestli byl uživatel odhlášen z důvodu vypršení časového intervalu prozradí metoda
$user->getLogoutReason(), která vrací buď konstantu Nette\Security\UserStorage::LogoutInactivity
(vypršel časový limit) nebo UserStorage::LogoutManual (odhlášen metodou logout()).
Autentikátor
Jde o objekt, který ověřuje přihlašovací údaje, tedy zpravidla jméno a heslo. Triviální podobou je třída Nette\Security\SimpleAuthenticator, kterou můžeme nadefinovat v konfiguraci:
security:
users:
# jméno: heslo
frantisek: tajneheslo
katka: jestetajnejsiheslo
Toto řešení je vhodné spíš pro testovací účely. Ukážeme si, jak vytvořit autentikátor, který bude ověřovat přihlašovací údaje oproti databázové tabulce.
Autentikátor je objekt implementující rozhraní Nette\Security\Authenticator s metodou
authenticate(). Jejím úkolem je buď vrátit tzv. identitu nebo vyhodit výjimku
Nette\Security\AuthenticationException. Bylo by možné u ní ještě uvést chybový kód k jemnějšímu
rozlišení vzniklé situace: Authenticator::IdentityNotFound a 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, // nebo pole více rolí
['name' => $row->username],
);
}
}
Třída MyAuthenticator komunikuje s databází prostřednictvím Nette
Database Explorer a pracuje s tabulkou users, kde je v sloupci username přihlašovací jméno
uživatele a ve sloupci password otisk hesla. Po ověření
jména a hesla vrací identitu, která nese ID uživatele, jeho roli (sloupec role v tabulce) a pole
s dalšími daty (v našem případě uživatelské jméno).
Autentikátor ještě přidáme do konfigurace jako službu DI kontejneru:
services:
- MyAuthenticator
Události $onLoggedIn, $onLoggedOut
Objekt Nette\Security\User má události
$onLoggedIn a $onLoggedOut, můžete tedy přidat callbacky, které se vyvolají po úspěšném
přihlášení resp. po odhlášení uživatele.
$user->onLoggedIn[] = function () {
// uživatel byl právě přihlášen
};
Identita
Identita představuje soubor informací o uživateli, který vrací autentikátor a který se následně uchovává v session
a získáváme jej pomocí $user->getIdentity(). Můžeme tedy získat id, role a další uživatelská data, tak
jak jsme si je předali v autentikátoru:
$user->getIdentity()->getId();
// funguje i zkratka $user->getId();
$user->getIdentity()->getRoles();
// uživatelská data jsou dostupná jako properties
// jméno, které jsme si předali v MyAuthenticator
$user->getIdentity()->name;
Co je důležité, tak že při odhlášení pomocí $user->logout() se identita nesmaže a je nadále
k dispozici. Takže ačkoliv má uživatel identitu, nemusí být přihlášený. Pokud bychom chtěli identitu explicitně
smazat, odhlásíme uživatele voláním logout(true).
Díky tomu můžete nadále předpokládat, který uživatel je u počítače a například mu v e-shopu zobrazovat personalizované nabídky, nicméně zobrazit mu jeho osobní údaje můžete až po přihlášení.
Identita je objekt implementující rozhraní Nette\Security\IIdentity, výchozí implementací je Nette\Security\SimpleIdentity. A jak bylo zmíněno, udržuje se v session, takže pokud tedy například změníme roli některého z přihlášených uživatelů, zůstanou stará data v jeho identitě až do jeho opětovného přihlášení.
Úložiště přihlášeného uživatele
Dvě základní informace o uživateli, tedy zda-li je přihlášen a jeho identita, se zpravidla
přenášejí v session. Což lze změnit. Ukládání těchto informací má na starosti objekt implementující rozhraní
Nette\Security\UserStorage. K dispozici jsou dvě standardní implementace, první přenáší data v session a
druhá v cookie. Jde o třídy Nette\Bridges\SecurityHttp\SessionStorage a CookieStorage. Zvolit si
uložiště a nakonfigurovat jej můžete velmi pohodlně v konfiguraci security › authentication.
Dále můžete ovlivnit, jak přesně bude probíhat ukládání identity (sleep) a obnovování (wakeup).
Stačí, aby authenticator implementoval rozhraní Nette\Security\IdentityHandler. To má dvě metody:
sleepIdentity() se volá před zápisem identity do úložiště a wakeupIdentity() po jejím
přečtení. Metody mohou obsah identity upravit, případně ji nahradit novým objektem, který vrátí. Metoda
wakeupIdentity() může dokonce vrátit null, čímž uživatele jej odhlásí.
Jako příklad si ukážeme řešení časté otázky, jak aktualizovat role v identitě hned po načtení ze session.
V metodě wakeupIdentity() předáme do identity aktuální role např. z databáze:
final class Authenticator implements
Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
public function sleepIdentity(IIdentity $identity): IIdentity
{
// zde lze pozměnit identitu před zápisem do úložiště po přihlášení,
// ale to nyní nepotřebujeme
return $identity;
}
public function wakeupIdentity(IIdentity $identity): ?IIdentity
{
// aktualizace rolí v identitě
$userId = $identity->getId();
$identity->setRoles($this->facade->getUserRoles($userId));
return $identity;
}
A nyní se vrátíme k úložišti na bázi cookies. Dovoluje vám vytvořit web, kde se mohou přihlašovat uživatelé a
přitom nepotřebuje sessions. Tedy nepotřebuje zapisovat na disk. Ostatně tak funguje i web, který právě čtete, včetně
fóra. V tomto případě je implementace IdentityHandler nutností. Do cookie totiž budeme ukládat jen náhodný
token reprezentující přihlášeného uživatele.
Nejprve tedy v konfiguraci nastavíme požadované úložiště pomocí
security › authentication › storage: cookie.
V databázi si vytvoříme sloupec authtoken, ve kterém bude mít každý uživatel zcela náhodný, unikátní a neuhodnutelný řetězec o dostatečné délce
(alespoň 13 znaků). Úložiště CookieStorage přenáší v cookie pouze hodnotu
$identity->getId(), takže v sleepIdentity() originální identitu nahradíme za zástupnou s
authtoken v ID, naopak v metodě wakeupIdentity() podle authtokenu přečteme celou identitu
z databáze:
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);
// ověříme heslo
...
// vrátíme identitu se všemi údaji z databáze
return new SimpleIdentity($row->id, null, (array) $row);
}
public function sleepIdentity(IIdentity $identity): SimpleIdentity
{
// vrátíme zástupnou identitu, kde v ID bude authtoken
return new SimpleIdentity($identity->authtoken);
}
public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
{
// zástupnou identitu nahradíme plnou identitou, jako v authenticate()
$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
return $row
? new SimpleIdentity($row->id, null, (array) $row)
: null;
}
}
Více nezávislých přihlášení
Souběžně je možné v rámci jednoho webu a jedné session mít několik nezávislých přihlašujících se uživatelů. Pokud například chceme mít na webu oddělenou autentizaci pro administraci a veřejnou část, stačí každé z nich nastavit vlastní název:
$user->getStorage()->setNamespace('backend');
Je důležité pamatovat na to, abychom jmenný prostor nastavili vždy na všech místech patřících do dané části. Pakliže používáme presentery, nastavíme jmenný prostor ve společném předkovi pro danou část – obvykle BasePresenter. Učiníme tak rozšířením metody checkRequirements():
public function checkRequirements($element): void
{
$this->getUser()->getStorage()->setNamespace('backend');
parent::checkRequirements($element);
}
Více autentikátorů
Rozdělení aplikace na části s nezávislým přihlašováním většinou vyžaduje také různé autentikátory. Jakmile
bychom však v konfiguraci služeb zaregistrovali dvě třídy implementující Authenticator, Nette by nevědělo, který
z nich automaticky přiřadit objektu Nette\Security\User, a zobrazilo by chybu. Proto musíme pro autentikátory autowiring omezit tak, aby fungoval, jen když si někdo
vyžádá konkrétní třídu, např. FrontAuthenticator, čehož docílíme volbou autowired: self:
services:
-
create: FrontAuthenticator
autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private FrontAuthenticator $authenticator,
) {
}
}
Autentikátor objektu User nastavíme před voláním metody login(), takže obvykle v kódu formuláře, který ho přihlašuje:
$form->onSuccess[] = function (Form $form, \stdClass $data) {
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($data->username, $data->password);
// ...
};