Přejít k navigační liště

Zdroják » PHP » Nette Framework: Přihlašování uživatelů

Nette Framework: Přihlašování uživatelů

Články PHP, Různé

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.

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ě NetteWebSes­sion, 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í NetteSecurityI­Authenticator. 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 NetteSecurityAu­thenticationEx­ception 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í NetteSecurityI­Identity 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í.

Komentáře

Subscribe
Upozornit na
guest
17 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
aprilchild

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?

aprilchild

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:).

Timy

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 :-).

v6ak

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?

Mastodont

Z hlediska ochrany proti útokům ano, protože útočník neví, zda netrefil jméno nebo heslo.

v6ak

Ale v případě přítomnosti registračního formuláře se to asi dozví velmi snadno…

Martin Hassman

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.

Mastodont

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 …

Martin Štěpař

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.

Játro

Hele, a umi to OpenId? Ne? To je pak framwork na hownow! ;-)

MiKee

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.

Játro

On to byl spíš takový ftípek pro Davida :) http://phpfashion.com/odmitam-openid

MiKee

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? ;-)

Daan

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. :)

jar

Tohle

// zaregistrujeme autentizační handler
$user->setAuthentica­tionHandler(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ý.

Schmutzka

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

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.