Benutzeranmeldung (Authentifizierung)

Kaum eine Webanwendung kommt ohne einen Mechanismus zur Benutzeranmeldung und Überprüfung von Benutzerberechtigungen aus. In diesem Kapitel werden wir über Folgendes sprechen:

  • An- und Abmelden von Benutzern
  • Eigene Authentifikatoren

Installation und Anforderungen

In den Beispielen verwenden wir das Objekt der Klasse Nette\Security\User, das den aktuellen Benutzer repräsentiert und auf das Sie zugreifen können, indem Sie es sich mittels Dependency Injection übergeben lassen. In Presentern genügt es, $user = $this->getUser() aufzurufen.

Authentifizierung

Authentifizierung bedeutet Benutzeranmeldung, also der Prozess, bei dem überprüft wird, ob der Benutzer wirklich derjenige ist, für den er sich ausgibt. Üblicherweise weist er sich mit Benutzernamen und Passwort aus. Die Überprüfung führt der sogenannte autentikátor durch. Schlägt die Anmeldung fehl, wird eine Nette\Security\AuthenticationException geworfen.

try {
	$user->login($username, $password);
} catch (Nette\Security\AuthenticationException $e) {
	$this->flashMessage('Benutzername oder Passwort ist falsch');
}

Auf diese Weise melden Sie den Benutzer ab:

$user->logout();

Und die Feststellung, ob er angemeldet ist:

echo $user->isLoggedIn() ? 'ja' : 'nein';

Sehr einfach, nicht wahr? Und alle Sicherheitsaspekte übernimmt Nette für Sie.

In Presentern können Sie die Anmeldung in der Methode startup() überprüfen und einen nicht angemeldeten Benutzer zur Anmeldeseite weiterleiten.

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

Ablauf (Expiration)

Die Benutzeranmeldung läuft zusammen mit dem Ablauf des Speichers ab, der normalerweise die Session ist (siehe Einstellung Session-Ablauf). Es kann jedoch auch ein kürzeres Zeitintervall festgelegt werden, nach dessen Ablauf der Benutzer abgemeldet wird. Dazu dient die Methode setExpiration(), die vor login() aufgerufen wird. Geben Sie als Parameter eine Zeichenkette mit einer relativen Zeit an:

// Anmeldung läuft nach 30 Minuten Inaktivität ab
$user->setExpiration('30 minutes');

// Aufhebung der eingestellten Expiration
$user->setExpiration(null);

Ob der Benutzer aufgrund des Ablaufs des Zeitintervalls abgemeldet wurde, verrät die Methode $user->getLogoutReason(), die entweder die Konstante Nette\Security\UserStorage::LogoutInactivity (Zeitlimit abgelaufen) oder UserStorage::LogoutManual (mit der Methode logout() abgemeldet) zurückgibt.

Authenticator

Dies ist ein Objekt, das die Anmeldedaten überprüft, also normalerweise Name und Passwort. Eine triviale Form ist die Klasse Nette\Security\SimpleAuthenticator, die wir in der Konfiguration definieren können:

security:
	users:
		# Benutzername: Passwort
		frantisek: geheimespasswort
		katka: nochgeheimerespasswort

Diese Lösung eignet sich eher für Testzwecke. Wir zeigen Ihnen, wie Sie einen Authenticator erstellen, der Anmeldedaten anhand einer Datenbanktabelle überprüft.

Ein Authenticator ist ein Objekt, das das Interface Nette\Security\Authenticator mit der Methode authenticate() implementiert. Ihre Aufgabe ist es, entweder eine sogenannte Identität zurückzugeben oder eine Ausnahme Nette\Security\AuthenticationException zu werfen. Es wäre auch möglich, einen Fehlercode anzugeben, um die entstandene Situation genauer zu unterscheiden: Authenticator::IdentityNotFound und 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('Benutzer nicht gefunden.');
		}

		if (!$this->passwords->verify($password, $row->password)) {
			throw new Nette\Security\AuthenticationException('Ungültiges Passwort.');
		}

		return new SimpleIdentity(
			$row->id,
			$row->role, // oder ein Array mehrerer Rollen
			['name' => $row->username],
		);
	}
}

Die Klasse MyAuthenticator kommuniziert mit der Datenbank über Nette Database Explorer und arbeitet mit der Tabelle users, wobei in der Spalte username der Anmeldename des Benutzers und in der Spalte password der Passwort-Hash gespeichert ist. Nach Überprüfung von Name und Passwort gibt sie die Identität zurück, die die ID des Benutzers, seine Rolle (Spalte role in der Tabelle), über die wir später mehr sprechen werden, und ein Array mit weiteren Daten (in unserem Fall der Benutzername) enthält.

Den Authenticator fügen wir noch zur Konfiguration als Dienst des DI-Containers hinzu:

services:
	- MyAuthenticator

Ereignisse $onLoggedIn, $onLoggedOut

Das Objekt Nette\Security\User hat die Ereignisse $onLoggedIn und $onLoggedOut. Sie können also Callbacks hinzufügen, die nach erfolgreicher Anmeldung bzw. nach Abmeldung des Benutzers aufgerufen werden.

$user->onLoggedIn[] = function () {
	// Benutzer wurde gerade angemeldet
};

Identität

Die Identität repräsentiert eine Sammlung von Informationen über den Benutzer, die vom Authenticator zurückgegeben wird und anschließend in der Session gespeichert und mit $user->getIdentity() abgerufen wird. Wir können also die ID, Rollen und weitere Benutzerdaten abrufen, so wie wir sie im Authenticator übergeben haben:

$user->getIdentity()->getId();
// funktioniert auch als Abkürzung $user->getId();

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

// Benutzerdaten sind als Eigenschaften verfügbar
// Name, den wir in MyAuthenticator übergeben haben
$user->getIdentity()->name;

Wichtig ist, dass bei der Abmeldung mit $user->logout() die Identität nicht gelöscht wird und weiterhin verfügbar ist. Auch wenn der Benutzer eine Identität hat, muss er also nicht angemeldet sein. Wenn wir die Identität explizit löschen möchten, melden wir den Benutzer mit dem Aufruf logout(true) ab.

Dadurch können Sie weiterhin davon ausgehen, welcher Benutzer am Computer ist, und ihm beispielsweise im E-Shop personalisierte Angebote anzeigen. Seine persönlichen Daten können Sie ihm jedoch erst nach der Anmeldung anzeigen.

Die Identität ist ein Objekt, das das Interface Nette\Security\IIdentity implementiert, die Standardimplementierung ist Nette\Security\SimpleIdentity. Und wie erwähnt, wird sie in der Session gehalten. Wenn wir also beispielsweise die Rolle eines angemeldeten Benutzers ändern, bleiben die alten Daten in seiner Identität bis zu seiner erneuten Anmeldung erhalten.

Speicher für angemeldete Benutzer

Die beiden grundlegenden Informationen über den Benutzer, nämlich ob er angemeldet ist und seine identita, werden in der Regel in der Session übertragen. Dies kann jedoch geändert werden. Für die Speicherung dieser Informationen ist ein Objekt verantwortlich, das das Interface Nette\Security\UserStorage implementiert. Es stehen zwei Standardimplementierungen zur Verfügung: die erste überträgt Daten in der Session und die zweite in einem Cookie. Dies sind die Klassen Nette\Bridges\SecurityHttp\SessionStorage und CookieStorage. Sie können den Speicher auswählen und ihn sehr bequem in der Konfiguration security › authentication konfigurieren.

Weiterhin können Sie beeinflussen, wie genau das Speichern der Identität (sleep) und das Wiederherstellen (wakeup) ablaufen soll. Es genügt, wenn der Authenticator das Interface Nette\Security\IdentityHandler implementiert. Dieses hat zwei Methoden: sleepIdentity() wird vor dem Schreiben der Identität in den Speicher aufgerufen und wakeupIdentity() nach dem Lesen daraus. Die Methoden können den Inhalt der Identität ändern oder sie durch ein neues Objekt ersetzen, das sie zurückgeben. Die Methode wakeupIdentity() kann sogar null zurückgeben, wodurch der Benutzer abgemeldet wird.

Als Beispiel zeigen wir die Lösung einer häufigen Frage, wie die Rollen in der Identität sofort nach dem Laden aus der Session aktualisiert werden können. In der Methode wakeupIdentity() übergeben wir der Identität die aktuellen Rollen, z. B. aus der Datenbank:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// hier kann die Identität vor dem Schreiben in den Speicher nach der Anmeldung geändert werden,
		// aber das brauchen wir jetzt nicht
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// Aktualisierung der Rollen in der Identität
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

Und nun kehren wir zum Cookie-basierten Speicher zurück. Er ermöglicht es Ihnen, eine Website zu erstellen, auf der sich Benutzer anmelden können, ohne Sessions zu benötigen. Das heißt, es muss nicht auf die Festplatte geschrieben werden. Übrigens funktioniert auch die Website, die Sie gerade lesen, einschließlich des Forums, auf diese Weise. In diesem Fall ist die Implementierung von IdentityHandler eine Notwendigkeit. Im Cookie speichern wir nämlich nur ein zufälliges Token, das den angemeldeten Benutzer repräsentiert.

Zuerst stellen wir also in der Konfiguration den gewünschten Speicher mit security › authentication › storage: cookie ein.

In der Datenbank erstellen wir eine Spalte authtoken, in der jeder Benutzer eine völlig zufällige, einzigartige und nicht erratbare Zeichenkette ausreichender Länge (mindestens 13 Zeichen) hat. Der CookieStorage-Speicher überträgt im Cookie nur den Wert $identity->getId(), sodass wir in sleepIdentity() die ursprüngliche Identität durch eine Platzhalter-Identität mit authtoken in der ID ersetzen. Umgekehrt lesen wir in der Methode wakeupIdentity() anhand des Authtokens die gesamte Identität aus der Datenbank:

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);
		// Passwort überprüfen
		...
		// Identität mit allen Daten aus der Datenbank zurückgeben
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// Platzhalter-Identität zurückgeben, wobei die ID das Authtoken ist
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// Platzhalter-Identität durch die vollständige Identität ersetzen, wie in authenticate()
		$row = $this->db->fetch('SELECT * FROM user WHERE authtoken = ?', $identity->getId());
		return $row
			? new SimpleIdentity($row->id, null, (array) $row)
			: null;
	}
}

Mehrere unabhängige Anmeldungen

Es ist möglich, innerhalb einer Website und einer Session mehrere unabhängige angemeldete Benutzer gleichzeitig zu haben. Wenn wir beispielsweise auf einer Website eine getrennte Authentifizierung für die Administration und den öffentlichen Bereich haben möchten, reicht es aus, jeder einen eigenen Namen zuzuweisen:

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

Es ist wichtig zu bedenken, dass wir den Namespace immer an allen Stellen festlegen, die zu dem jeweiligen Teil gehören. Wenn wir Presenter verwenden, legen wir den Namespace im gemeinsamen Vorfahren für den jeweiligen Teil fest – normalerweise im BasePresenter. Wir tun dies, indem wir die Methode checkRequirements() erweitern:

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

Mehrere Authentifikatoren

Die Aufteilung der Anwendung in Teile mit unabhängiger Anmeldung erfordert meistens auch unterschiedliche Authentifikatoren. Sobald wir jedoch in der Dienstkonfiguration zwei Klassen registrieren, die Authenticator implementieren, wüsste Nette nicht, welche davon dem Objekt Nette\Security\User automatisch zugewiesen werden soll, und würde einen Fehler anzeigen. Daher müssen wir für Authentifikatoren das Autowiring einschränken, sodass es nur funktioniert, wenn jemand eine bestimmte Klasse anfordert, z. B. FrontAuthenticator, was wir durch die Wahl autowired: self erreichen:

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

Den Authenticator des User-Objekts legen wir vor dem Aufruf der Methode login() fest, also normalerweise im Code des Formulars, das ihn anmeldet:

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