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()
.
Ve verzi 3.0 měly rozhraní ještě prefix I
, takže názvy byly
Nette\Security\IUserStorage
, IAuthenticator
a IAuthorizator
atd. Dále třída
Nette\Security\SimpleIdentity
se jmenovala Nette\Security\dentity
.
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::LOGOUT_INACTIVITY
(vypršel časový limit) nebo UserStorage::LOGOUT_MANUAL
(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::IDENTITY_NOT_FOUND
a Authenticator::INVALID_CREDENTIAL
.
use Nette;
use Nette\Security\SimpleIdentity;
class MyAuthenticator implements Nette\Security\Authenticator
{
private $database;
private $passwords;
public function __construct(
Nette\Database\Explorer $database,
Nette\Security\Passwords $passwords
) {
$this->database = $database;
$this->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]
);
}
}
Poznámka: Ve verzi 3.0 bylo rozhraní jiné:
use Nette;
class MyAuthenticator implements Nette\Security\IAuthenticator
{
public function authenticate(array $credentials): Nette\Security\IIdentity
{
[$username, $password] = $credentials;
// ...
return new Nette\Security\Identity(
$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), o které si více řekneme později, 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
{
/** @var FrontAuthenticator */
private $authenticator;
public function __construct(FrontAuthenticator $authenticator)
{
$this->authenticator = $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);
// ...
};