Logowanie użytkownika (uwierzytelnianie)

Prawie żadna aplikacja internetowa nie może obejść się bez mechanizmu logowania i uwierzytelniania użytkowników. W tym rozdziale omówimy:

  • logowanie użytkowników w i z
  • niestandardowe uwierzytelniacze

Instalacja i wymagania

W przykładach użyjemy obiektu klasy Nette\Security\User, który reprezentuje aktualnego użytkownika i do którego można uzyskać dostęp, zlecając jego przekazanie przez dependency injection. W prezenterze wystarczy wywołać $user = $this->getUser().

Uwierzytelnianie

Uwierzytelnianie oznacza logowanie użytkownika, czyli proces weryfikacji, czy użytkownik jest tym, za kogo się podaje. Zwykle objawia się to nazwą użytkownika i hasłem. Uwierzytelnianie odbywa się za pomocą tzw. authenticatora. Jeśli logowanie nie powiedzie się, Nette\Security\AuthenticationException jest wyrzucany.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('Uživatelské jméno nebo heslo je nesprávné');
}

W ten sposób wylogowuje się użytkownika:

$user->logout();

I stwierdzając, że jest zalogowany:

echo $user->isLoggedIn() ? 'ano' : 'ne';

Bardzo proste, prawda? A Nette załatwia za Ciebie wszystkie aspekty bezpieczeństwa.

W presenterech można uwierzytelnić logowanie w metodzie startup() i przekierować niezalogowanego użytkownika na stronę logowania.

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

Wygaśnięcie

Login użytkownika wygasa wraz z wygaśnięciem pamięci masowej, którą zwykle jest sesja (patrz ustawienia wygasania sesji). Można jednak ustawić krótszy interwał czasowy, po którym użytkownik zostanie wylogowany. Odbywa się to poprzez wywołanie metody setExpiration() przed login(). Jako parametr podaj ciąg znaków z czasem względnym:

// login wygasa po 30 minutach bezczynności
$user->setExpiration('30 minutes');

// anulowanie wygaśnięcia zestawu
$user->setExpiration(null);

Metoda $user->getLogoutReason() informuje, czy użytkownik został wylogowany z powodu timeoutu i zwraca stałą Nette\Security\UserStorage::LogoutInactivity (timeout wygasł) lub UserStorage::LogoutManual (wylogowany metodą logout()).

Authenticator

Jest to obiekt, który uwierzytelnia poświadczenia logowania, zwykle nazwę użytkownika i hasło. Trywialną formą jest klasa Nette\Security\SimpleAuthenticator, którą można zdefiniować w konfiguracji:

security:
	users:
		# nazwa: hasło
		frantisek: tajne hasło
		katka: jestetajnejsiheslo

To rozwiązanie jest bardziej odpowiednie do celów testowych. Pokażemy jak stworzyć authenticator, który będzie walidował poświadczenia logowania względem tabeli w bazie danych.

Autentykator jest obiektem implementującym interfejs Nette\Security\Authenticator z metodą authenticate() Jego zadaniem jest albo zwrócenie tzw. tożsamości, albo rzucenie wyjątku Nette\Security\AuthenticationException. Można by jeszcze dołączyć kod błędu, aby dokładniej rozróżnić zaistniałą sytuację: Authenticator::IdentityNotFound i Authenticator::InvalidCredential.

use Nette;
use Nette\Security\SimpleIdentity;

class MyAuthenticator implements Nette\Security\Authenticator
{
	public function __construct(
		private Nette\Database\Explorer $database,
		private Nette\Security\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],
		);
	}
}

Klasa MyAuthenticator komunikuje się z bazą danych poprzez Nette Database Explorer i pracuje z tabelą users, gdzie kolumna username zawiera nazwę logowania użytkownika, a kolumna password – odcisk palca hasła. Po sprawdzeniu poprawności nazwy i hasła zwraca tożsamość, która niesie ze sobą identyfikator użytkownika, jego rolę (kolumna role w tabeli), o której więcej powiemy później, oraz pole z dodatkowymi danymi (w naszym przypadku nazwę użytkownika).

Autentykator dodamy jeszcze do konfiguracji jako usługę kontenera DI:

services:
	- MyAuthenticator

Zdarzenia $onLoggedIn, $onLoggedOut

Obiekt Nette\Security\User ma zdarzenia $onLoggedIn i $onLoggedOut, więc można dodać callbacki, które odpalają się odpowiednio po udanym logowaniu i wylogowaniu.

$user->onLoggedIn[] = function () {
	// użytkownik właśnie się zalogował
};

Tożsamość

Tożsamość to zestaw informacji o użytkowniku zwrócony przez authenticator, który następnie jest przechowywany w sesji i pobierany za pomocą $user->getIdentity(). Możemy zatem pobrać id, role i inne dane użytkownika przekazane nam w authenticatorze:

$user->getIdentity()->getId();
// działa również skrót $user->getId();

$user->getIdentity()->getRoles();

// dane użytkownika są dostępne jako właściwości
// nazwa, którą przekazaliśmy w MyAuthenticator
$user->getIdentity()->name;

Co ważne, gdy wylogujemy się za pomocą $user->logout() tożsamość nie jest usuwana i nadal jest dostępna. Więc chociaż użytkownik ma tożsamość, nie musi być zalogowany. Gdybyśmy chcieli jawnie usunąć tożsamość, wylogowalibyśmy użytkownika, wywołując logout(true).

W ten sposób można nadal zakładać, który użytkownik znajduje się na komputerze i np. pokazywać mu spersonalizowane oferty w sklepie internetowym, ale można mu pokazać jego dane osobowe dopiero po zalogowaniu.

Tożsamość jest obiektem, który implementuje interfejs Nette\Security\IIdentity, domyślną implementacją jest Nette\Security\SimpleIdentity. I jak wspomniano, jest utrzymywany w sesji, więc jeśli zmienimy rolę zalogowanego użytkownika, na przykład, stare dane pozostaną w jego tożsamości, dopóki nie zalogują się ponownie.

Magazyn zalogowanego użytkownika

Dwie podstawowe informacje o użytkowniku, czyli to, czy jest zalogowany i jego tożsamość, są zwykle przekazywane w ramach sesji. Które można zmienić. Przechowywaniem tych informacji zajmuje się obiekt implementujący interfejs Nette\Security\UserStorage Istnieją dwie standardowe implementacje, pierwsza przekazuje dane w sesji, a druga w ciasteczku. Są to klasy Nette\Bridges\SecurityHttp\SessionStorage i CookieStorage. Przechowywanie można wybrać i skonfigurować bardzo wygodnie w konfiguracji Security › authentication.

Co więcej, możesz dokładnie kontrolować, jak będzie przebiegać przechowywanie tożsamości (sleep) i jej odzyskiwanie (wakeup). Potrzebujesz tylko, aby authenticator zaimplementował interfejs Nette\Security\IdentityHandler. Ma on dwie metody: sleepIdentity() jest wywoływany przed zapisaniem tożsamości do magazynu oraz wakeupIdentity() jest wywoływany po jej odczytaniu. Metody mogą modyfikować zawartość tożsamości lub zastąpić ją nowym obiektem, który zwraca. Metoda wakeupIdentity() może nawet zwrócić null, wylogowując tym samym użytkownika.

Jako przykład pokażemy rozwiązanie częstego pytania, jak zaktualizować role w tożsamości zaraz po jej pobraniu z sesji. W metodzie wakeupIdentity() przekazujemy do tożsamości aktualną rolę, np. z bazy danych:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// tutaj tożsamość może być zmieniona przed zapisem do repozytorium po zalogowaniu,
		// ale nie jest nam to teraz potrzebne.
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// aktualizacja ról w tożsamości
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

A teraz wracamy do przechowywania na podstawie plików cookie. Pozwala stworzyć stronę, na której użytkownicy mogą się zalogować, a mimo to nie potrzebuje sesji. To znaczy, że nie musi zapisywać na dysku. Przecież tak właśnie działa strona, którą teraz czytasz, w tym forum. W tym przypadku wdrożenie IdentityHandler jest koniecznością. W pliku cookie przechowujemy jedynie losowy token reprezentujący zalogowanego użytkownika.

Więc najpierw ustawiamy pożądany magazyn w konfiguracji za pomocą security › authentication › storage: cookie.

W bazie danych utworzymy kolumnę authtoken, w której każdy użytkownik będzie miał całkowicie losowy, unikalny i nieodgadniony ciąg znaków o odpowiedniej długości (co najmniej 13 znaków). Magazyn CookieStorage przenosi tylko wartość $identity->getId() w ciasteczku, dlatego w sleepIdentity() zastępujemy oryginalną tożsamość placeholderem z authtoken w ID, natomiast w metodzie wakeupIdentity() odczytujemy z bazy całą tożsamość zgodnie z authtokenem:

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
		...
		// w tym celu należy użyć danych z bazy danych.
		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;
	}
}

Wiele niezależnych loginów

Możliwe jest posiadanie wielu niezależnych użytkowników logujących się w tym samym czasie w ramach jednej witryny i jednej sesji. Na przykład, jeśli chcesz mieć oddzielne uwierzytelnianie dla części administracyjnej i publicznej witryny, po prostu ustaw oddzielną nazwę dla każdego z nich:

$user->getStorage()->setNamespace('backend');

Należy pamiętać, aby zawsze ustawiać przestrzeń nazw na wszystkich stronach należących do tej sekcji. Jeśli używamy prezenterów, ustawiamy przestrzeń nazw we wspólnym przodku dla części – zwykle BasePresenter. Robimy to poprzez rozszerzenie metody checkRequirements():

public function checkRequirements($element): void
{
	$this->getUser()->getStorage()->setNamespace('backend');
	parent::checkRequirements($element);
}

Wielokrotne uwierzytelnianie

Podzielenie aplikacji na części z niezależnym logowaniem zwykle wymaga również różnych uwierzytelniaczy. Jednak gdy tylko zarejestrowaliśmy w konfiguracji usługi dwie klasy implementujące Authenticator, Nette nie wiedziałoby, którą z nich automatycznie przypisać do obiektu Nette\Security\User i wyświetliłoby błąd. Dlatego musimy ograniczyć autowiring dla Authenticatorów, aby działał tylko wtedy, gdy ktoś zażąda konkretnej klasy, np. FrontAuthenticator, co uzyskuje się poprzez wybranie autowired: self:

services:
	-
		create: FrontAuthenticator
		autowired: self
class SignPresenter extends Nette\Application\UI\Presenter
{
	public function __construct(
		private FrontAuthenticator $authenticator,
	) {
	}
}

Autentykator obiektu User ustawiamy przed wywołaniem metody login(), a więc zwykle w kodzie formularza, który go loguje:

$form->onSuccess[] = function (Form $form, \stdClass $data) {
	$user = $this->getUser();
	$user->setAuthenticator($this->authenticator);
	$user->login($data->username, $data->password);
	// ...
};
wersja: 4.0