Nette Framework: Přihlašová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í. Pojďme se podívat, jak tyto úlohy řeší Nette Framework.
Seriál: Začínáme s Nette Framework (17 dílů)
- Nette Framework: zvyšte svoji produktivitu 10. 3. 2009
- Nette Framework: Odvšivujeme 17. 3. 2009
- Nette Framework: MVC & MVP 24. 3. 2009
- Nette Framework: Refactoring 31. 3. 2009
- Nette Framework: Chytré šablony 7. 4. 2009
- Nette Framework: adresářová struktura aplikace 14. 4. 2009
- Nette Framework: AJAX 21. 4. 2009
- Nette Framework: AJAX (pokračování) 28. 4. 2009
- Nette Framework: AJAX (dokončení) 5. 5. 2009
- Nette Framework: Sessions 12. 5. 2009
- Nette Framework: Přihlašování uživatelů 19. 5. 2009
- Nette Framework: Ověřování oprávnění a role 26. 5. 2009
- Nette Framework: Neprůstřelné formuláře 2. 6. 2009
- Nette Framework: Neprůstřelné formuláře II 9. 6. 2009
- Nette Framework: Neprůstřelné formuláře III 16. 6. 2009
- Nette Framework: Cache 23. 6. 2009
- Nette Framework: Co se do seriálu nevešlo? 30. 6. 2009
Nálepky:
Nejprve troška terminologie. Celé téma si rozdělíme do dvou okruhů: autentizace a autorizace. Pod 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á. V drtivé většině aplikací se ověřuje uživatelské jméno a heslo. Naopak při autorizaci se zjišťuje, zda má již autentizovaný uživatel dostatečná oprávnění pro přístup k určitému souboru či pro provedení nějaké akce. Autorizaci si necháme do příštího pokračování.
Chcete se naučit o Nette víc?
Akademie Root.cz školení Vývoj webových aplikací v Nette Framework. Kurz je určen všem programátorům v PHP, kteří se chtějí naučit tvořit webové aplikace rychle a kvalitně, bez bezpečnostních děr. Jako aplikační rámec slouží Nette Framework. Školí sám autor Nette – David Grudl. Máte zájem o jiné školení? Napište nám!
Autentizace
Přihlašování uživatelů je oblast velmi úzce související s ochranou osobních údajů a zabezpečením aplikace. Jelikož PHP nenabízí žádnou standardní implementaci, jde také bohužel o oblast bezbřehé programátorské „kreativity“. Lze se setkat s odstrašujícími případy, kdy programátoři například ukládají hesla do cookies a nebo vytvářejí jiné sofistikované bezpečnostní díry.
Nette Framework se snaží tuto díru zacelit. A zároveň přihlašování zjednodušit až na naprosté minimum. Tím jsou dvě metody authenticate()
(přihlásit) a signOut()
(odhlásit), plus dotazovací metoda isAuthenticated()
sdělující, zda je uživatel nyní přihlášen.
O realizační stránku se stará třída NetteWebUser
. Ta je, stejně jako v případě NetteWebSession, singleton, proto nevytváříme její instanci přímo, ale vrátí ji metoda Environment::getUser()
. Používá se zhruba tímto způsobem:
require 'Nette/loader.php';
$user = Environment::getUser();
// přihlášení uživatele
$username = ...
$password = ...
$user->authenticate($username, $password);
// je přihlášen?
echo $user->isAuthenticated() ? 'ano' : 'ne';
// odhlášení
$user->signOut();
Aby příklad fungoval, je potřeba napsat rutinu, která provede ověření uživatelského jména a hesla. Této rutině se říká autentizační handler a jde o objekt implementující rozhraní NetteSecurityIAuthenticator. To má jedinou metodu authenticate()
. Implementace, která ověřuje přihlašovací údaje oproti databázové tabulce, může vypadat třeba takto:
require 'Nette/loader.php';
// pokud používáte verzi pro PHP 5.3, odkomentujte následující řádek:
// use NetteObject, NetteSecurityIAuthenticator, NetteSecurityAuthenticationException, NetteSecurityIdentity;
class MyAuthenticator extends Object implements IAuthenticator
{
public function authenticate(array $credentials)
{
$username = $credentials[self::USERNAME];
$password = sha1($credentials[self::PASSWORD] . $credentials[self::USERNAME]);
// přečteme záznam o uživateli z databáze
$row = dibi::fetch('SELECT realname, password FROM users WHERE login=%s', $username);
if (!$row) { // uživatel nenalezen?
throw new AuthenticationException("User '$username' not found.", self::IDENTITY_NOT_FOUND);
}
if ($row->password !== $password) { // hesla se neshodují?
throw new AuthenticationException("Invalid password.", self::INVALID_CREDENTIAL);
}
return new Identity($row->realname); // vrátíme identitu
}
}
Autentizační handler si zaslouží hlubší rozbor. Z pohledu návrhu aplikace podle vzoru MVP jde o součást modelu, přičemž samotnou autentizaci zpravidla iniciuje presenter. Nette Framework vás tak vede k oddělení ověření údajů od prezentační vrstvy.
Úkolem handleru je buď vrátit tzv. identitu v případě úspěchu, nebo vyhodit výjimku. Nette definuje výjimku NetteSecurityAuthenticationException a několik chybových kódů, které můžete využít k formálnímu popisu vzniklé chyby. (Nicméně na to, jakou výjimku vyhodíte, se žádná omezení nekladou, nakonec bude je zachytávat a ošetřovat opět váš kód.)
V případě úspěšné autentizace vrácí handler identitu, což je objekt implementující rozhraní NetteSecurityIIdentity a popisující aktuálního uživatele. Popis může obsahovat libovolné údaje, povinné je uživatelské jméno (což nemusí být nutně totéž, jako přihlašovací jméno) a role (o těch si povíme více v příštím dílu). K identitě se dostaneme přes getter getIdentity()
:
$user = Environment::getUser();
if ($user->isAuthenticated()) {
echo 'Prihlášen uživatel: ', $user->getIdentity()->getName();
} else {
echo 'Uživatel není přihlášen';
}
Odhlášení
Jak už jsem zmínil, uživatele odhlásí metoda signOut()
. Při odhlášení se však nesmaže uživatelská identita, kterou máme i nadále k dispozici. Pokud bychom chtěli identitu explicitně smazat, odhlásíme uživatele voláním signOut(TRUE)
.
Kromě manuálního odhlášení nabízí Nette Framework i automatické odhlášení po uplynutí časového intervalu nebo zavření okna prohlížeče. K tomu slouží metoda setExpiration()
, kterou volejte vždy před samotnou autentizací. Metoda setExpiration()
jako parametr akceptuje relativní čas v sekundách nebo UNIX timestamp, v aktuální verzi frameworku je možné použít i velmi srozumitelný textový zápis. Druhý parametr stanoví, zda se má uživatel odhlásit při zavření okna prohlížeče:
// přihlášení vyprší po 30 minutách neaktivity nebo zavření okna prohlížeče
$user->setExpiration('+ 30 minutes');
// přihlášení vyprší po 2 dnech
$user->setExpiration('+ 2 days', FALSE);
Dokonce je možné zjistit, z jakého důvodu k poslednímu odhlášení došlo (viz metoda getSignOutReason
).
Suma sumárum
Kompletní postup přihlašování uživatele pak vypadá asi takto:
require 'Nette/loader.php';
require 'MyAuthenticator.php';
// pokud používáte verzi pro PHP 5.3, odkomentujte následující řádek:
// use NetteEnvironment, NetteSecurityAuthenticationException;
// přihlašovací údaje
$username = ...
$password = ...
$user = Environment::getUser();
// zaregistrujeme autentizační handler
$user->setAuthenticationHandler(new MyAuthenticator);
// nastavíme expiraci
$user->setExpiration('+ 30 minutes');
try {
// pokusíme se přihlásit uživatele...
$user->authenticate($username, $password);
// ...a v případě úspěchu presměrujeme na další stránku
Environment::getHttpResponse()->redirect('index.php');
} catch (AuthenticationException $e) {
echo 'Chyba: ', $e->getMessage();
}
Příště se podíváme na uživatelské role a ověřování uživatelských oprávnění.
Autor článku je vývojář na volné noze, specializuje se na návrh a programování moderních webových aplikací. Vyvíjí open-source knihovny Texy, dibi a Nette Framework a pravidelně pořádá školení pro tvůrce webových aplikací, které od podzimu 2009 nabídne kurz vývoje AJAXových aplikací.
mozna je lepsi nerozlisovat mezi "uzivatel neexistuje" a "chybne heslo" a schovat je pod jednu hlasku, o malinko se zvysi prah bezpecnosti. podporuje Nette i sliding expiration?
Je to sice pravda, ale pekně mě štve, když si nemůžu vzpomenout na své přihlašovací údaje na nějakém serveru a on mi to právě tímto způsobem neusnadní ;)
ad sliding expiration: ano, používá se.
mne to stve taky :) ale stejne jsem radsi za tuhle trochu "security through obscurity", byt je to casto spise o lenosti programatora s osetrovanim stavu nez bezpecnosti (specialne kdyz mi pote poslou mailem moje stare heslo ;).
kazdopadne moc pekna prace s Nette. skoda jen, ze neni cca 2004, ale uznavam, ze to o moc driv diky samotnemu PHP (resp. bastlu, co tehdy byl) neslo. na druhou stranu, kdyz uz nekdo ve 14-ti (nebo v kolika dneska juniori zacinaji) ma zacit s PHP, at je to s Nette a ne nicim jinym:).
Já souhlasím s Davidem, minimálně po stránce uživatelské je to noční můra, když se někam přihlašuji a píše mi to „nepodařilo se přihlásit“, zatímco já zkouším jiné kombinace hesel a přitom mám špatně samotný login. Tudy, prosím, ne :-).
To je fakt bezpečnější? S registračním formulářem ani ne – co když tam uživatel zkusí existující jméno? To taky budeme mlžit?
Z hlediska ochrany proti útokům ano, protože útočník neví, zda netrefil jméno nebo heslo.
Ale v případě přítomnosti registračního formuláře se to asi dozví velmi snadno…
Jak píše v6ak, pokud trefu do loginu oznamujeme v registračním formuláři, byly již dveře pootevřeny a nějaká ochrana přihlašovacího formuláře je již zavřít nedokáže.
Omlouvám se, to jsem špatně pochopil – registrace je ovšem něco úplně jiného než přihlašování a kód bude vypadat taky jinak …
Ano, kód bude vypadat jinak. Jde o to, že: pokud registrační formulář řekne "uživatelské jméno již existuje" je to úplně to samé, jako když přihlašovací formulář řekne "špatně zadané heslo" – v obou případech s jistotou víme, že jméno existuje a je platné. (z čehož logicky plyne, že i při hlášce "přihlášení se nezdařilo" víme, že chyba musí být ve špatně zadaném hesle)
Navíc, informování uživatele lze zpracovat v presenteru, nic nám tedy nebrání v tom, aby model vracel korektní chyby, a my je v presenteru logovali a uživateli zobrazili pouze obecnou chybovou hlášku.
Hele, a umi to OpenId? Ne? To je pak framwork na hownow! ;-)
Hmmm. Někdy platí „jaký nick, takový uživatel“. A nemyslím
slavného detektiva Nicka Cartera. Až jednou napíšete alespoň desetinu tak
promakaného a programátorsky čistého a především funkčního kódu,
přijďte se pochlubit, vy Játro.
On to byl spíš takový ftípek pro Davida :) http://phpfashion.com/odmitam-openid
O-ou, to se omlouvám, nepochopil jsem, ačkoli jsem Davidův článek
o OpenID četl. Ale nějak mě tady hrozně prudí všichni ti věční
rejpalové a tak jsem Vás hodil s nimi do jednoho pytle. Jak tam
bylo? ;-)
Tato cast sa mi naozaj paci, pretoze je tu vysvetlena cista prax. Ziadne
terminy, poucky, rozdelenia… :)
Problem, rozdeleny na podproblemy a v zavere zhrnute do jedneho scriptu. Way
to go, z takychto materialov sa imho uci najlepsie.
Teraz si to skusim v praxi sam otestovat a cim viac takychto veci budem
robit, tym lepsie sa do toho dostanem. Casom clovek prirodzenou cestou a
skusenostami pochopi aj tie poucky a rozdelenia. :)
Tohle
// zaregistrujeme autentizační handler
$user->setAuthenticationHandler(new MyAuthenticator);
jsem hledal v tutoriálu Akrabat (http://nettephp.com/cs/tutorialy) a
nakonec zjistil, že to jde nastavit také v config.ini a je to tam. (http://nettephp.com/…tte-web-user#…).
Tak to sem píšu, kdyby nemohl najít ještě někdo jiný.
Na základě hlasování došlo k přejmenování metod!
authenticate() → login()
signOut() → logout()
isAuthenticated() → isLoggedIn()
getSignOutReason() → getLogoutReason()
$onAuthenticated → $onLoggedIn
$onSignedOut → $onLoggedOut
http://forum.nette.org/cs/4149-2010-04-13-prejmenovani-metod-user-authenticate-signout-isauthenticated-login-logout-isloggedin