Autentifikace

Nette poskytuje způsob jak naprogramovat autentifikaci na našich stránkách, ale do ničeho nás nenutí. Implementace je pouze na nás. Nette obsahuje rozhraní Nette\Security\IAuthenticator, které vyžaduje pouze jednu metodu authenticate, která ověřuje uživatele jakkoliv budeme chtít.

Existuje mnoho možností, jak může být uživatel ověřen. Nejčastější způsob ověření je pomocí hesla (uživatel poskytne své jméno, nebo email a heslo), ale jsou zde i jiné způsoby. Možná znáte tlačítka typu „Přihlásit pomocí Facebooku“, nebo přihlášení pomocí Google/Twitter/GitHub na některých stránkách. S Nette můžeme mít jakoukoliv přihlašovací metodu, nebo je klidně můžeme kombinovat. Je to jen na nás.

Obyčejně bychom si nejspíš napsali vlastní authenticator, ale pro tento jednoduchý malý blog použijeme SimpleAuthenticator, který je v Nette předpřipraven. Poskytuje přihlášení na základě hesla a uživatelského jména, které je uloženo v konfiguračním souboru. Přidáme tedy security sekci do souboru config.neon (a nezapomeneme změnit heslo):

security:
    users:
        admin: secret  # user 'admin', password 'secret'

Nette automaticky vytvoří službu s názvem authenticator v DI kontejneru.

Více si můžete přečíst o Dependency Injection zde a o konfiguraci zde

Přihlašovací formulář

Nyní máme autentifikaci připravenu a musíme připravit uživatelské rozhraní pro přihlášení. Vytvořme tedy nový presenter s názvem SignPresenter, který:

  • zobrazí přihlašovací formulář (s přihlašovacím jménem a heslem)
  • po odeslání formuláře ověří uživatele
  • poskytne možnost odhlášení

Začneme s přihlašovacím formulářem. Již víme jak formuláře v presenterech fungují. Vytvoříme si tedy presenter SignPresenter a zapíšeme metodu createComponentSignInForm. Měl by vypadat nějak takto:

namespace App\Presenters;

use Nette;
use Nette\Application\UI\Form;


class SignPresenter extends Nette\Application\UI\Presenter
{
    protected function createComponentSignInForm(): Form
    {
        $form = new Form;
        $form->addText('username', 'Uživatelské jméno:')
            ->setRequired('Prosím vyplňte své uživatelské jméno.');

        $form->addPassword('password', 'Heslo:')
            ->setRequired('Prosím vyplňte své heslo.');

        $form->addSubmit('send', 'Přihlásit');

        $form->onSuccess[] = [$this, 'signInFormSucceeded'];
        return $form;
    }
}

Jsou zde políčka pro uživatelské jméno a heslo.

View

Formulář se bude vykreslovat v šabloně app/presenters/templates/Sign/in.latte:

{block content}
<h1 n:block=title>Přihlášení</h1>

{control signInForm}

Přihlašovací callback

Dále doplníme callback pro přihlášení uživatele, který bude volán hned po úspěšném odeslání formuláře.

Callback pouze převezme uživatelské jméno a heslo, které uživatel vyplnil a předá to authenticatoru. Po přihlášení přesměrujeme na úvodní stránku.

public function signInFormSucceeded(Form $form, \stdClass $values): void
{
    try {
        $this->getUser()->login($values->username, $values->password);
        $this->redirect('Homepage:');

    } catch (Nette\Security\AuthenticationException $e) {
        $form->addError('Nesprávné přihlašovací jméno nebo heslo.');
    }
}

Metoda User::login() vyhodí výjimku, pokud uživatelské jméno a heslo nesouhlasí s údaji v konfiguračním souboru. Jak již víme, toto může vyústit v červenou chybovou stránku, nebo v produkčním módu ve zprávu informující o server erroru. To však nechceme. Proto tuto výjimku zachytíme a předáme hezkou, uživatelsky přívětivou chybovou zprávu do formuláře.

Jakmile dojde k chybě ve formuláři, stránka s formulářem se překreslí a nad formulářem se zobrazí hezká zpráva informující uživatele, že vyplnil špatné přihlašovací jméno nebo heslo.

Formulář pro nové příspěvky

Prvně zabezpečíme formulář pro přidávání nových příspěvků. Ten je definován v presenteru PostPresenter a renderován v šabloně app/presenters/templates/Post/create.latte. Naším prvním cílem je znemožnit přístup na stránku uživatelům, kteří nejsou přihlášeni.

Šablony presenterů

Vytvoříme action metodu actionCreate, která přesměruje nepřihlášené uživatele na formulář s přihlášením.

public function actionCreate(): void
{
    if (!$this->getUser()->isLoggedIn()) {
        $this->redirect('Sign:in');
    }
}

Stejným způsobem zabezpečíme i editovací stránku, takže jednoduše přidáme tyto tři řádky také sem:

public function actionEdit(int $postId): void
{
    if (!$this->getUser()->isLoggedIn()) {
        $this->redirect('Sign:in');
    }

Skrytí odkazů

Neautorizovaný uživatel nemůže vidět stránku create ani edit, ale stále na ně může vidět odkazy. Ty bychom měli také schovat. Jeden takový odkaz je v šabloně app/presenters/templates/Homepage/default.latte a měl by být vidět pouze přihlášeným uživatelům.

Můžeme jej schovat využitím n:makra jménem n:if. Pokud je tato podmínka false, celý tag <a> včetně obsahu zůstane skrytý.

<a n:href="Post:create" n:if="$user->loggedIn">Vytvořit příspěvek</a>

což je zkratka následujícího zápisu (neplést s tag-if)

{if $user->loggedIn}<a n:href="Post:create">Editovat příspěvek</a>{/if}

Stejným způsobem skryjeme také odkaz v šabloně app/presenters/templates/Post/show.latte.

Callbacky formulářů

Poslední a nejdůležitější věc je zabezpečit callbacky formulářů. Protože jsou komponenty znovupoužitelné, mohou být vykresleny v mnoha pohledech. A protože mohou být vykresleny v jakémkoliv pohledu svého presenteru, mohou být také odeslány z kterékoliv akce a to i z těch, které ani nejsou definovány. To znamená, že i když pohled create není vykreslen, úpravou URL adresy může útočník formulář tak jako tak odeslat (a přidat/editovat příspěvek).

Tomu můžeme zabránit přidáním jednoduchého if, který přidáme na začátek metody postFormSucceeded:

public function postFormSucceeded(Form $form, \stdClass $values): void
{
    if (!$this->getUser()->isLoggedIn()) {
        $this->error('Pro vytvoření, nebo editování příspěvku se musíte přihlásit.');
    }

Je to velmi jednoduché, ale nesmíme na to nikdy zapomenout, protože je opravdu kritické zabezpečení aplikace.

Odkaz na přihlášení

Jak se vlastně dostaneme na přihlašovací stránku? Není zde žádný odkaz, který by na ni vedl. Tak si ho tedy přidáme do šablony app/presenters/templates/@layout.latte. Pokuste se najít vhodné místo, může to být téměř kdekoliv.

<ul class="navig">
    <li><a n:href="Homepage:">Články</a></li>
    {if $user->loggedIn}
        <li><a n:href="Sign:out">Odhlásit</a></li>
    {else}
        <li><a n:href="Sign:in">Přihlásit</a></li>
    {/if}
</ul>

Pokud není uživatel přihlášen, zobrazí se odkaz „Přihlásit“. V opačném případě se zobrazí odkaz „Odhlásit“. Tuto akce také doplníme do SignPresenter.

Jelikož uživatele po odhlášení okamžitě přesměrujeme, není potřeba žádná šablona. Odhlášení vypadá takto:

public function actionOut(): void
{
    $this->getUser()->logout();
    $this->flashMessage('Odhlášení bylo úspěšné.');
    $this->redirect('Homepage:');
}

Pouze se zavolá metoda logout() a následně se zobrazí hezká zpráva potvrzující úspěšné odhlášení.

Shrnutí

Nyní máme odkaz odkazující na nový presenter, který požaduje od uživatelů přihlašovací údaje a umožňuje jim přihlásit se. K ověření uživatele jsme použili SimpleAuthenticator a přihlašovací údaje máme v konfiguračním souboru. Jelikož se jedná o velmi jednoduchou aplikaci, nepotřebujeme více účtů. Také jsme zabezpečili všechny akce a formuláře, takže přidávat a editovat příspěvky mohou pouze přihlášení uživatelé.