Přihlašování & oprávnění
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ů
- ověření uživatelských oprávnění
- vlastních autentikátorech a autorizá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é');
}
Ohlášení uživatele:
$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.
Ještě lze nastavit časový interval, po jehož uplynutí dojde k odhlášení uživatele (jinak se odhlásí s expirací session). 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í expirace
$user->setExpiration(null);
Expirace musí být nastavena na stejnou nebo nižší hodnotu, než jakou má expirace session.
Důvod odhlášení prozradí metoda $user->getLogoutReason()
, která vrací buď konstantu
Nette\Security\IUserStorage::INACTIVITY
(vypršel časový limit) nebo IUserStorage::MANUAL
(odhlášen
metodou logout()
).
V presenterech můžete ověřit přihlášení v metodě startup()
:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isLoggedIn()) {
$this->redirect('Sign:in');
}
}
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\IAuthenticator 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: IAuthenticator::IDENTITY_NOT_FOUND
a
IAuthenticator::INVALID_CREDENTIAL
.
use Nette;
class MyAuthenticator implements Nette\Security\IAuthenticator
{
private $database;
private $passwords;
public function __construct(Nette\Database\Context $database, Nette\Security\Passwords $passwords)
{
$this->database = $database;
$this->passwords = $passwords;
}
public function authenticate(array $credentials): Nette\Security\IIdentity
{
[$username, $password] = $credentials;
$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 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í 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 $user->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\Identity. 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í.
Autorizace
Autorizace zjišťuje, zda má uživatel dostatečná oprávnění například pro přístup k určitému zdroji či pro provedení nějaké akce. Autorizace předpokládá předchozí úspěšnou autentizaci, tj. že uživatel je přihlášen.
U velmi jednoduchých webů s administrací, kde se nerozlišují oprávnění uživatelů, je možné jako autorizační
kritérium použít již známou metodu isLoggedIn()
. Jinými slovy: jakmile je uživatel přihlášen, má veškerá
oprávnění a naopak.
if ($user->isLoggedIn()) { // je uživatel přihlášen?
deleteItem(); // pak má k operaci oprávnění
}
Role
Smyslem rolí je nabídnout přesnější řízení oprávnění a zůstat nezávislý na uživatelském jméně. Každému
uživateli hned při přihlášení přiřkneme jednu či více rolí, ve kterých bude vystupovat. Role mohou být jednoduché
řetězce například admin
, member
, guest
, apod. Uvádí se jako druhý parametr
konstruktoru Identity
, buď jako řetězec nebo pole řetězců – rolí.
Jako autorizační kritérium nyní použijeme metodu isInRole()
, která prozradí, zda uživatel vystupuje
v dané roli:
if ($user->isInRole('admin')) { // je uživatel v roli admina?
deleteItem(); // pak má k operaci oprávnění
}
Jak už víte, po odhlášení uživatele se nemusí smazat jeho identita. Tedy i nadále metoda getIdentity()
vrací objekt Identity
, včetně všech udělených rolí. Nette Framework vyznává princip „less code, more
security“, kdy méně psaní vede k více zabezpečenému kódu, proto při zjišťování rolí nemusíte ještě ověřovat,
zda je uživatel přihlášený. Metoda isInRole()
pracuje s efektivními rolemi, tj. pokud je uživatel
přihlášen, vychází z rolí uvedených v identitě, pokud přihlášen není, má automaticky speciální roli
guest
.
Autorizátor
Kromě rolí zavedeme ještě pojmy zdroj a operace:
- role je vlastnost uživatele – např. moderátor, redaktor, návštěvník, zaregistrovaný uživatel, správce…
- zdroj (resource) je nějaký logický prvek webu – článek, stránka, uživatel, položka v menu, anketa, presenter, …
- operace (operation) je nějaká konkrétní činnost, kterou uživatel může či nemůže se zdrojem dělat – například smazat, upravit, vytvořit, hlasovat, …
Autorizátor je objekt, který rozhoduje, zda má daná role povolení provést určitou operaci s určitým
zdrojem. Jde o objekt implementující rozhraní Nette\Security\IAuthorizator s jedinou metodu
isAllowed()
:
class MyAuthorizator implements Nette\Security\IAuthorizator
{
public function isAllowed($role, $resource, $operation): bool
{
if ($role === 'admin') {
return true;
}
if ($role === 'user' && $resource === 'article') {
return true;
}
...
return false;
}
}
Autorizátor přidáme do konfigurace jako službu DI kontejneru:
services:
- MyAuthorizator
A následuje příklad použití. Pozor, tentokrát voláme metodu Nette\Security\User::isAllowed()
, nikoliv
autorizátor, takže tam není první parametr $role
. Tato metoda volá MyAuthorizator::isAllowed()
postupně pro všechny uživatelovy role a vrací true, pokud alespoň jedna z nich má povolení.
if ($user->isAllowed('file')) { // může uživatel dělat cokoliv se zdrojem 'file'?
useFile();
}
if ($user->isAllowed('file', 'delete')) { // může nad zdrojem 'file' provést 'delete'?
deleteFile();
}
Oba parametry jsou volitelné, výchozí hodnota null
má význam cokoliv.
Permission ACL
Nette přichází s vestavěnou implementací autorizátoru, a to třídou Nette\Security\Permission poskytující programátorovi lehkou a flexibilní ACL (Access Control List) vrstvu pro řízení oprávnění a přístupů. Práce s ní spočívá v definici rolí, zdrojů a jednotlivých oprávnění. Přičemž role a zdroje umožňují vytvářet hierarchie. Na vysvětlenou si ukážeme příklad webové aplikace:
guest
: nepřihlášený návštěvník, který může číst a procházet veřejnou část webu, tzn. číst články, komentáře a volit v anketáchregistered
: přihlášený registrovaný uživatel, který navíc může komentovatadministrator
: může spravovat články, komentáře i ankety
Nadefinovali jsme si tedy určité role (guest
, registered
a admin
) a zmínili zdroje
(article
, comment
, poll
), ke kterým mohou uživatelé s nějakou rolí přistupovat nebo
provádět určité operace (view
, vote
, add
, edit
).
Vytvoříme instanci třídy Permission a nadefinujeme role. Lze přitom využít tzv. dědičnost rolí, která
zajistí, že např. uživatel s rolí administrátor
může dělat i to co obyčejný návštěvník webu (a
samozřejmě i více).
$acl = new Nette\Security\Permission;
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // registered dědí od guest
$acl->addRole('administrator', 'registered'); // a od něj dědí administrator
Nyní nadefinujeme i seznam zdrojů, ke kterým mohou uživatelé přistupovat.
$acl->addResource('article');
$acl->addResource('comment');
$acl->addResource('poll');
I zdroje mohou používat dědičnost, bylo by možné například zadat
$acl->addResource('perex', 'article')
.
A teď to nejdůležitější. Nadefinujeme mezi nimi pravidla určující, kdo co může s čím dělat:
// nejprve nikdo nemůže dělat nic
// nechť guest může prohlížet články, komentáře i ankety
$acl->allow('guest', ['article', 'comment', 'poll'], 'view');
// a v anketách navíc i hlasovat
$acl->allow('guest', 'poll', 'vote');
// registrovaný dědí práva od guesta, dáme mu navíc právo komentovat
$acl->allow('registered', 'comment', 'add');
// administrátor může prohlížet a editovat cokoliv
$acl->allow('administrator', $acl::ALL, ['view', 'edit', 'add']);
Co když chceme někomu zamezit k určitému zdroji přístup?
// administrátor nemůže editovat ankety, to by bylo nedemokratické
$acl->deny('administrator', 'poll', 'edit');
Nyní, když máme vytvořený seznam pravidel, můžeme jednoduše klást autorizační dotazy:
// může guest prohlížet články?
$acl->isAllowed('guest', 'article', 'view'); // true
// může guest editovat články?
$acl->isAllowed('guest', 'article', 'edit'); // false
// může guest hlasovat v anketách?
$acl->isAllowed('guest', 'poll', 'vote'); // true
// může guest komentovat?
$acl->isAllowed('guest', 'comment', 'add'); // false
Totéž platí pro registrovaného uživatele, ten však může i komentovat:
$acl->isAllowed('registered', 'article', 'view'); // true
$acl->isAllowed('registered', 'comment', 'add'); // true
$acl->isAllowed('registered', 'comment', 'edit'); // false
Administrátor může editovat vše, kromě anket:
$acl->isAllowed('administrator', 'poll', 'vote'); // true
$acl->isAllowed('administrator', 'poll', 'edit'); // false
$acl->isAllowed('administrator', 'comment', 'edit'); // true
Oprávění mohou také být vyhodnocována dynamicky a můžeme rozhodnutí nechat na vlastním callbacku, kterému se předají všechny parametry:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
return ...;
};
$acl->allow('registered', 'comment', null, $assertion);
Jak ale třeba řešit situaci, kdy nestačí jen názvy rolí a zdrojů, ale chtěli bychom definovat, že třeba role
registered
může editovat zdroj article
jen pokud je jeho autorem? Místo řetězců použijeme
objekty, role bude objekt Nette\Security\IRole a zdroj Nette\Security\IResource. Jejich metody
getRoleId()
resp. getResourceId()
budou vracet původní řetezce:
class Registered implements Nette\Security\IRole
{
public $id;
public function getRoleId(): string
{
return 'registered';
}
}
class Article implements Nette\Security\IResource
{
public $authorId;
public function getResourceId(): string
{
return 'article';
}
}
A nyní vytvoříme pravidlo:
$assertion = function (Permission $acl, string $role, string $resource, string $privilege): bool {
$role = $acl->getQueriedRole(); // objekt Registered
$resource = $acl->getQueriedResource(); // objekt Article
return $role->id === $resource->authorId;
};
$acl->allow('registered', 'article', 'edit', $assertion);
A dotaz na ACL se provede předáním objektů:
$user = new Registered(...);
$article = new Article(...);
$acl->isAllowed($user, $article, 'edit');
Role může dědit od jiné role či od více rolí. Co se ale stane, pokud má jeden předek akci zakázanou a druhý povolenou? Jaké budou práva potomka? Určuje se to podle váhy role – poslední uvedená role v seznamu předků má největší váhu, první uvedená role tu nejmenší. Více názorné je to z příkladu:
$acl = new Nette\Security\Permission;
$acl->addRole('admin');
$acl->addRole('guest');
$acl->addResource('backend');
$acl->allow('admin', 'backend');
$acl->deny('guest', 'backend');
// případ A: role admin má menší váhu než role guest
$acl->addRole('john', ['admin', 'guest']);
$acl->isAllowed('john', 'backend'); // false
// případ B: role admin má větší váhu než guest
$acl->addRole('mary', ['guest', 'admin']);
$acl->isAllowed('mary', 'backend'); // true
Role a zdroje lze i odebírat (removeRole()
, removeResource()
), lze revertovat i pravidla
(removeAllow()
, removeDeny()
). Pole všech přímých rodičovských rolí vrací
getRoleParents()
, zda od sebe dvě entity dědí vrací roleInheritsFrom()
a
resourceInheritsFrom()
.
Přidání jako služby
Námi vytvořené ACL si potřebujeme předat do konfigurace jako službu, aby jej začal používat objekt $user
,
tedy aby bylo možné používat v kódu např. $user->isAllowed('article', 'view')
. Za tím účelem si na něj
napíšeme továrnu:
namespace App\Model;
class AuthorizatorFactory
{
public static function create(): Nette\Security\Permission
{
$acl = new Nette\Security\Permission;
$acl->addRole(...);
$acl->addResource(...);
$acl->allow(...);
return $acl;
}
}
A přidáme ji do konfigurace:
services:
- App\Model\AuthorizatorFactory::create
V presenterech pak můžete ověřit oprávnění například v metodě startup()
:
protected function startup()
{
parent::startup();
if (!$this->getUser()->isAllowed('backend')) {
$this->error('Forbidden', 403);
}
}
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í IAuthenticator, 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:
-
factory: 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, $values)
{
$user = $this->getUser();
$user->setAuthenticator($this->authenticator);
$user->login($values['username'], $values['password']);
...
}