Přihlašování & oprávnění uživatelů
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í
- zabezpečení proti zranitelnostem
- vlastní autentikátory a autorizátory
- Access Control List
Než se do tématu pustíme, řekněme si, že v příkladech budeme
používat službu user, což je objekt třídy Nette\Http\User.
Službu získáme ze systémového
kontejneru pomocí $user = $container->user nebo
v presenteru voláním $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.
Uživatele přihlásíme pomocí jména a hesla:
$user->login($username, $password);
Zjistíme, zda je uživatel přihlášen:
echo $user->isLoggedIn() ? 'ano' : 'ne';
A odhlásíme jej:
$user->logout();
Velmi jednoduché, viďte?
Přihlašování vyžaduje u uživatele povolené cookies; jiná metoda přihlašování není bezpečná!
Kromě odhlášení metodu logout() lze uživatele odhlásit po
uplynutí časového intervalu nebo zavření prohlížeče. K nastavení
slouží metoda setExpiration(), kterou voláme při
přihlašování. Jako parametr lze uvést relativní čas v sekundách, UNIX
timestamp nebo textový zápis. Druhý parametr stanoví, zda se má uživatel
odhlásit při zavření prohlížeče.
// přihlášení vyprší po 30 minutách neaktivity nebo zavření prohlížeče
$user->setExpiration('+ 30 minutes', TRUE);
// přihlášení vyprší po 2 dnech
$user->setExpiration('+ 2 days', FALSE);
// odhlásit uživatele až zavře prohlížeč (bez časového limitu)
$user->setExpiration(0, TRUE);
Expirace musí být nastavena na stejnou nebo nižší hodnotu, než jakou má expirace sessions.
Důvod posledního odhlášení prozradí metoda
$user->getLogoutReason(), která vrací buď konstantu
IUserStorage::INACTIVITY (vypršel časový limit),
IUserStorage::BROWSER_CLOSED (uživatel zavřel prohlížeč) nebo
IUserStorage::MANUAL (volání metody logout()).
Aby však příklad s přihlášením fungoval, je potřeba napsat objekt, který ověří uživatelské jméno a heslo. Říká se mu autentikátor. Jeho triviální podobou je třída Nette\Security\SimpleAuthenticator, která dostane v konstruktoru seznam uživatelů a hesel jakožto asociativní pole:
$authenticator = new Nette\Security\SimpleAuthenticator(array(
'john' => 'IJ^%4dfh54*',
'kathy' => '12345', // Kathy, this is a very weak password!
));
$user->setAuthenticator($authenticator);
Pokud přihlašovací údaje nejsou platné, vyhodí autentikátor výjimku Nette\Security\AuthenticationException.
try {
// pokusíme se přihlásit uživatele...
$user->login($username, $password);
// ...a v případě úspěchu presměrujeme na další stránku
$this->redirect(...);
} catch (Nette\Security\AuthenticationException $e) {
echo 'Chyba: ', $e->getMessage();
}
Autentikátor obvykle budeme nastavovat přímo v konfiguračním souboru, v takovém
případě se objekt vytvoří, až když ho bude skutečně potřeba. Vyše
uvedený příklad by se v config.neon zapsal takto:
common:
services:
authenticator:
class: Nette\Security\SimpleAuthenticator
arguments: [[
john: IJ^%4dfh54*
kathy: 12345
]]
Vlastní autentikátor
Napíšeme si vlastní autentikátor, který bude ověřovat přihlašovací
údaje oproti databázové tabulce. Každý autentikátor je implementací
rozhraní Nette\Security\IAuthenticator
mající jedinou metodu authenticate(). Jejím úkolem je buď
vrátit tzv. identitu nebo vyhodit výjimku
Nette\Security\AuthenticationException. Framework definuje
i několik chybových kódů, které můžeme využít k formálnímu popisu
vzniklé chyby (např. IAuthenticator::IDENTITY_NOT_FOUND nebo
IAuthenticator::INVALID_CREDENTIAL.)
use Nette\Security as NS;
class MyAuthenticator extends Nette\Object implements NS\IAuthenticator
{
public $connection;
function __construct(Nette\Database\Connection $connection)
{
$this->connection = $connection;
}
function authenticate(array $credentials)
{
list($username, $password) = $credentials;
$row = $this->connection->table('users')
->where('username', $username)->fetch();
if (!$row) {
throw new NS\AuthenticationException('User not found.');
}
if ($row->password !== md5($password)) {
throw new NS\AuthenticationException('Invalid password.');
}
return new NS\Identity($row->id, $row->role);
}
}
Třída MyAuthenticator komunikuje s databází prostřednictvím Nette\Database a pracuje s tabulkou
users, kde je v sloupci username přihlašovací
jméno uživatele a ve sloupci password otisk hesla (MD5). Po
ověření jména a hesla vrací identitu, která nese ID uživatele a jeho roli
(sloupec role v tabulce), o které si více řekneme později.
Autentikátor by se v konfiguračním souboru nastavil takto:
common:
services:
authenticator:
class: MyAuthenticator
arguments: [@database]
database:
class: Nette\Database\Connection
arguments: [..., ..., ...] # přístupové údaje k DB
Identita
Identita představuje soubor informací o uživateli, jak nám jej vrátí
autentikátor. Jedná se o objekt implementující rozhraní Nette\Security\IIdentity,
výchozí implementací je Nette\Security\Identity.
Třída disponuje metodou getId(), která vrací ID uživatele
(např. primární klíč v databázi), a getRoles(), která
vrátí seznam všech rolí, ve kterých uživatel vystupuje. K uživatelským
datům lze přistupovat jako k proměnným objektu.
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).
Služba user třídy Nette\Http\User
udržuje identitu v session a využívá ji ke všem autorizačním procesům.
K identitě se dostaneme přes funkci getIdentity():
if ($user->isLoggedIn()) {
echo 'Prihlášen uživatel: ', $user->getIdentity()->getId();
// nebo kratší způsob
echo 'Prihlášen uživatel: ', $user->id;
} else {
echo 'Uživatel není přihlášen';
}
Autorizace
Autorizace zjišťuje, zda má uživatel dostatečná oprávnění, například pro přístup k souboru či pro provedení nějaké akce. Autorizace předpokládá předchozí úspěšnou autentizaci, tj. že uživatel je přihlášen.
Autorizace se může v Nette Framework vyhodnocovat na základě členství uživatele v určitých skupinách či přidělených rolích. Začněme ale od úplného začátku.
U jednoduchých webů s administrací, kde se nerozlišují různá
oprávnění uživatelů, je možné jako autorizační kritérium použít již
známou metodu isLoggedIn(). Řečeno srozumitelnějším jazykem:
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í, ale 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 pojmenovány 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ávající princip „less code, more security“, kdy méně psaní vede
k více zabezpečenému kódu, nechce nutit programátora všude psát if
($user->isLoggedIn() && $user->isInRole('admin')) a proto
metoda isInRole() pracuje s efektivními rolemi. 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
Autorizátor je ten, kdo umí rozhodnout, zda má uživatel oprávnění
provést určitou akci. Jde o objekt implementující rozhraní Nette\Security\IAuthorizator
s jedinou metodu isAllowed(), jejímž úkolem je rozhodnout, zda
má daná role povolení provést určitou operaci s určitým
zdrojem.
- 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 (privilege) 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, …
Rámcová podoba implementace vypadá takto:
class MyAuthorizator extends Nette\Object
implements Nette\Security\IAuthorizator
{
function isAllowed($role, $resource, $privilege)
{
return ...; // vrací TRUE nebo FALSE
}
}
A následuje příklad použití:
// zaregistrujeme autorizátor
$user->setAuthorizator(new MyAuthorizator);
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();
}
Nezaměňujte si dvě různé metody isAllowed:
jedna patří autorizátoru, druhá třídě User, kde už není první parametr
$role.
Protože uživatel může vystupovat ve více rolích, povolení dostane, pokud alespoň jedna role má povolení. Oba parametry jsou volitelné, výchozí hodnota nese význam všechny.
Permission ACL
Nette Framework disponuje předpřipravenou implementací autorizátoru, a to třídou Nette\Security\Permission poskytující programátorovi lehkou a flexibilní ACL vrstvu pro řízení práv a přístupu. 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 psát a spravovat články, komentáře i ankety
Nadefinovali jsme si tedy určité role (guest,
registered a admin) a zmínili zdroje
(article, comments, 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 uživatelské 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;
// definujeme role
$acl->addRole('guest');
$acl->addRole('registered', 'guest'); // registered dědí od guest
$acl->addRole('administrator', 'registered'); // a od něj dědí administrator
Docela triviální že? Tímto zajistíme, že se nám vlastnosti budou přenášet z rodičovské role na potomky.
Za zmínku stojí metoda getRoleParents(), která vrací pole se
všemi rodičovskými rolemi a také metoda roleInheritsFrom(),
která zjistí, zda od sebe dědí dvě role. Jejich použití:
$acl->roleInheritsFrom('administrator', 'guest'); // TRUE
$acl->getRoleParents('administrator'); // array('guest', 'registered')
Nyní je čas nadefinovat i seznam zdrojů, ke kterým mohou uživatelé přistupovat.
$acl->addResource('article');
$acl->addResource('comments');
$acl->addResource('poll');
I zdroje/objekty mohou používat dědičnost. Metody pro dotazování se na
hierarchii zdrojů jsou podobné jako u rolí, liší se jen názvy:
resourceInheritsFrom(), removeResource().
A teď to nejdůležitější. Samotné role a objekty by nám byly k ničemu, musíme mezi nimi vytvořit ještě pravidla, určující, kdo co může s čím dělat:
// nejprve nikdo nemůže dělat nic
// guest nechť může prohlížet obsah veřejné části, hlasovat v anketách
$acl->allow('guest', array('article', 'comments', 'poll'), 'view');
// registrovaný dědí právo od guesta, ale má i právo komentovat
$acl->allow('registered', 'comments', 'add');
// administrace může prohlížet a editovat cokoliv
$acl->allow('administrator', Permission::ALL, array('view', 'edit', 'add'));
Co když chceme někomu zamezit do určitého zdroje přístup?
// administrátor nebude mít zbytečně přístup do osobního nastavení uživatelů
$acl->deny('adminitrator', 'user', 'mySettings');
// mužům nebudeme zbytečně zobrazovat nudné kategorie produktů
$acl->deny('male', 'products', array('beauty', 'puppies', 'pink'));
Nyní když máme vytvořený seznam pravidel, můžeme jednoduše klást autorizační dotazy:
// může guest prohlížet články?
echo $acl->isAllowed('guest', 'article', 'view'); // TRUE
// může guest editovat články?
echo $acl->isAllowed('guest', 'article', 'edit'); // FALSE
// může guest hlasovat v anketách?
echo $acl->isAllowed('guest', 'poll', 'vote'); // TRUE
// může guest komentovat?
echo $acl->isAllowed('guest', 'comments', 'add'); // FALSE
Totéž platí pro registrovaného uživatele, ten však komentovat už může:
echo $acl->isAllowed('registered', 'article', 'view'); // TRUE
echo $acl->isAllowed('registered', 'comments', 'add'); // TRUE
echo $acl->isAllowed('registered', 'backend', 'view'); // FALSE
Administrátor může editovat vše:
echo $acl->isAllowed('administrator', 'poll', 'vote'); // TRUE
echo $acl->isAllowed('administrator', 'poll', 'edit'); // TRUE
echo $acl->isAllowed('administrator', 'comment', 'edit'); // TRUE
Práva administrátora lze nadefinovat i bez omezení tzn. bez rodičů, od kterých by dědil nějaká omezení. Vypadalo by to asi takto:
$acl->addRole('supervisor');
$acl->allow('supervisor'); // všechna práva a zdroje pro administrátora
Kdykoliv za běhu aplikace můžeme i odebrat roli metodou
removeRole(), zdroj odebere removeResource(), pravidlo
removeAllow() nebo removeDeny().
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 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', array('admin', 'guest'));
$acl->isAllowed('john', 'backend'); // FALSE
// případ B: role admin má větší váhu než guest
$acl->addRole('mary', array('guest', 'admin'));
$acl->isAllowed('mary', 'backend'); // TRUE
Více aplikací v jednom prostoru
V rámci jedné aplikace (serveru, session) může fungovat více aplikací, s tím, že si každá spravuje přihlašování samostatně. Stačí každé nastavit vlastní jmenný prostor:
$user->setNamespace('forum');
Události $onLoggedIn, $onLoggedOut
Služba user disponuje události $onLoggedIn a
$onLoggedOut například pro jednoduché přidání callbacku pro
logování autorizačních aktivit na webu. Událost $onLoggedIn je
volána po úspěšném přihlášení, událost $onLoggedOut po
odhlášení uživatele.
Komentáře 
Josef Kašpar | 28. 6. 2011, 18:37 | comment
http://doc.nette.org/cs/security#… –
zápis neon arguments: [,...,...] tečky by bylo vhodné nahradit
příkladem
http://doc.nette.org/cs/security#… –
supervisor – : $acl->allow('supervisor'); – není jasné,
že jde o toto: $acl->allow(‚supervisor‘, Permision:ALL,Permision:ALL);
a že Permision:ALL=null
Ginny | 4. 1. 2012, 2:54 | bug
// guest nechť může prohlížet obsah veřejné části, hlasovat v anketách
$acl->allow(‚guest‘, array(‚article‘, ‚comments‘, ‚poll‘), ‚view‘);
Má tam být ještě to vote:
$acl->allow(‚guest‘, array(‚article‘, ‚comments‘, ‚poll‘), array(‚view‘, ‚vote‘));
Protože o kousek dál se to testuje:
// může guest hlasovat v anketách?
echo $acl->isAllowed(‚guest‘, ‚poll‘, ‚vote‘); // TRUE


Jan Tvrdík | 24. 6. 2011, 8:59 | comment
Role a zdroje nemusí být pouze řetězce, ale mohou to být také objekty. Viz Šikovnější Permission.