Přihlašování uživatelů (Autentizace)

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ů
  • vlastních autentikátorech

Instalace a požadavky

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

Ve verzi 3.0 měly rozhraní ještě prefix I, takže názvy byly Nette\Security\IUserStorage, IAuthenticator a IAuthorizator atd. Dále třída Nette\Security\SimpleIdentity se jmenovala Nette\Security\dentity.

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é');
}

Tímto způsobem uživatele odhlásíte:

$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.

V presenterech můžete ověřit přihlášení v metodě startup() a nepřihlášeného uživatele přesměrovat na přihlašovací stránku.

protected function startup()
{
	parent::startup();
	if (!$this->getUser()->isLoggedIn()) {
		$this->redirect('Sign:in');
	}
}

Expirace

Přihlášení uživatele expiruje společně s expirací úložiště, kterým je obvykle session (viz nastavení expirace session). Lze ale nastavit i kratší časový interval, po jehož uplynutí dojde k odhlášení uživatele. 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í nastavené expirace
$user->setExpiration(null);

Jestli byl uživatel odhlášen z důvodu vypršení časového intervalu prozradí metoda $user->getLogoutReason(), která vrací buď konstantu Nette\Security\UserStorage::LOGOUT_INACTIVITY (vypršel časový limit) nebo UserStorage::LOGOUT_MANUAL (odhlášen metodou logout()).

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\Authenticator 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: Authenticator::IDENTITY_NOT_FOUND a Authenticator::INVALID_CREDENTIAL.

use Nette;
use Nette\Security\SimpleIdentity;

class MyAuthenticator implements Nette\Security\Authenticator
{
	private $database;
	private $passwords;

	public function __construct(
		Nette\Database\Explorer $database,
		Nette\Security\Passwords $passwords
	) {
		$this->database = $database;
		$this->passwords = $passwords;
	}

	public function authenticate(string $username, string $password): SimpleIdentity
	{
		$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 SimpleIdentity(
			$row->id,
			$row->role, // nebo pole více rolí
			['name' => $row->username]
		);
	}
}

Poznámka: Ve verzi 3.0 bylo rozhraní jiné:

use Nette;

class MyAuthenticator implements Nette\Security\IAuthenticator
{
	public function authenticate(array $credentials): Nette\Security\IIdentity
	{
		[$username, $password] = $credentials;

		// ...

		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\Userudá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í pomocí $user->logout() 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 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\SimpleIdentity. 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í.

Úložiště přihlášeného uživatele

Dvě základní informace o uživateli, tedy zda-li je přihlášen a jeho identita, se zpravidla přenášejí v session. Což lze změnit. Ukládání těchto informací má na starosti objekt implementující rozhraní Nette\Security\UserStorage. K dispozici jsou dvě standardní implementace, první přenáší data v session a druhá v cookie. Jde o třídy Nette\Bridges\SecurityHttp\SessionStorage a CookieStorage. Zvolit si uložiště a nakonfigurovat jej můžete velmi pohodlně v konfiguraci security › authentication.

Dále můžete ovlivnit, jak přesně bude probíhat ukládání identity (sleep) a obnovování (wakeup). Stačí, aby authenticator implementoval rozhraní Nette\Security\IdentityHandler. To má dvě metody: sleepIdentity() se volá před zápisem identity do úložiště a wakeupIdentity() po jejím přečtení. Metody mohou obsah identity upravit, případně ji nahradit novým objektem, který vrátí. Metoda wakeupIdentity() může dokonce vrátit null, čímž uživatele jej odhlásí.

Jako příklad si ukážeme řešení časté otázky, jak aktualizovat role v identitě hned po načtení ze session. V metodě wakeupIdentity() předáme do identity aktuální role např. z databáze:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// zde lze pozměnit identitu před zápisem do úložiště po přihlášení,
		// ale to nyní nepotřebujeme
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// aktualizace rolí v identitě
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

A nyní se vrátíme k úložišti na bázi cookies. Dovoluje vám vytvořit web, kde se mohou přihlašovat uživatelé a přitom nepotřebuje sessions. Tedy nepotřebuje zapisovat na disk. Ostatně tak funguje i web, který právě čtete, včetně fóra. V tomto případě je implementace IdentityHandler nutností. Do cookie totiž budeme ukládat jen náhodný token reprezentující přihlášeného uživatele.

Nejprve tedy v konfiguraci nastavíme požadované úložiště pomocí security › authentication › storage: cookie.

V databázi si vytvoříme sloupec authtoken, ve kterém bude mít každý uživatel zcela náhodný, unikátní a neuhodnutelný řetězec o dostatečné délce (alespoň 13 znaků). Úložiště CookieStorage přenáší v cookie pouze hodnotu $identity->getId(), takže v sleepIdentity() originální identitu nahradíme za zástupnou s authtoken v ID, naopak v metodě wakeupIdentity() podle authtokenu přečteme celou identitu z databáze:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function authenticate(string $username, string $password): SimpleIdentity
	{
		$row = $this->db->fetch('SELECT * FROM user WHERE username = ?', $username);
		// ověříme heslo
		...
		// vrátíme identitu se všemi údaji z databáze
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// vrátíme zástupnou identitu, kde v ID bude authtoken
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// zástupnou identitu nahradíme plnou identitou, jako v authenticate()
		$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
		return $row
			? new SimpleIdentity($row->id, null, (array) $row)
			: null;
	}
}

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í Authenticator, 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:
	-
		create: 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, \stdClass $data) {
	$user = $this->getUser();
	$user->setAuthenticator($this->authenticator);
	$user->login($data->username, $data->password);
	// ...
};
verze: 4.0 3.x 2.x