Benutzer authentifizieren

Fast keine Webanwendung kommt ohne einen Mechanismus zum An- und Abmelden von Benutzern und zur Prüfung ihrer Berechtigungen aus. In diesem Kapitel sprechen wir über:

  • das An- und Abmelden von Benutzern
  • eigene Authenticatoren

Installation und Anforderungen

In den Beispielen verwenden wir ein Objekt der Klasse Nette\Security\User, das den aktuellen Benutzer repräsentiert und das Sie sich per Dependency Injection übergeben lassen. In Presentern genügt der Aufruf $user = $this->getUser().

Authentifizierung

Authentifizierung bedeutet die Anmeldung eines Benutzers, also den Vorgang, bei dem die Identität eines Benutzers geprüft wird. Der Benutzer weist sich üblicherweise mit Benutzernamen und Passwort aus. Die Prüfung übernimmt der sogenannte Authenticator. Schlägt die Anmeldung fehl, wird eine Nette\Security\AuthenticationException geworfen.

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

So melden Sie den Benutzer ab:

$user->logout();

Und so stellen Sie fest, ob der Benutzer angemeldet ist:

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

Ganz einfach, nicht wahr? Und um alle Sicherheitsaspekte kümmert sich Nette für Sie.

In Presentern können Sie die Anmeldung in der Methode startup() prüfen und nicht angemeldete Benutzer auf die Anmeldeseite weiterleiten.

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

Ablauf

Die Anmeldung des Benutzers läuft zusammen mit dem Ablauf des Speichers ab, was üblicherweise die Session ist (siehe die Einstellung des Session-Ablaufs). Sie können jedoch auch ein kürzeres Zeitintervall setzen, nach dem der Benutzer abgemeldet wird. Dazu dient die Methode setExpiration(), die vor login() aufgerufen wird. Übergeben Sie als Argument einen String mit einer relativen Zeit:

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

// den gesetzten Ablauf aufheben
$user->setExpiration(null);

Die Methode $user->getLogoutReason() verrät, ob der Benutzer abgemeldet wurde, weil das Zeitintervall abgelaufen ist. Sie gibt entweder die Konstante Nette\Security\User::LogoutInactivity (die Zeitgrenze ist abgelaufen) oder User::LogoutManual (die Methode logout() wurde aufgerufen) zurück.

Authenticator

Das ist ein Objekt, das die Anmeldedaten prüft, typischerweise Benutzername und Passwort. Eine ganz einfache Form ist die Klasse Nette\Security\SimpleAuthenticator, die sich in der Konfiguration definieren lässt:

security:
	users:
		# Benutzername: Passwort
		johndoe: 'secret123'
		kathy: 'evenmoresecretpassword'

Statt Passwörtern im Klartext können Sie auch ihre Hashes eintragen; siehe Konfiguration.

Diese Lösung eignet sich eher zu Testzwecken. Wir zeigen Ihnen, wie Sie einen Authenticator erstellen, der die Anmeldedaten gegen eine Datenbanktabelle prüft.

Ein Authenticator ist ein Objekt, das das Interface Nette\Security\Authenticator mit der Methode authenticate() implementiert. Ihre Aufgabe ist es, entweder eine Identity zurückzugeben oder eine Nette\Security\AuthenticationException zu werfen. Es wäre außerdem möglich, einen Fehlercode anzugeben, um die Situation feiner zu unterscheiden: Authenticator::IdentityNotFound oder 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 von Rollen
			['name' => $row->username],
		);
	}
}

Die Klasse MyAuthenticator kommuniziert über den Nette Database Explorer mit der Datenbank und arbeitet mit der Tabelle users, in der die Spalte username den Anmeldenamen des Benutzers und die Spalte password den Passwort-Hash enthält. Nach der Prüfung von Name und Passwort gibt sie die Identity zurück, die die ID des Benutzers, seine Rolle (die Spalte role in der Tabelle), über die wir später mehr sprechen, und ein Array mit weiteren Daten enthält (in unserem Fall den Benutzernamen).

Den Authenticator fügen wir der Konfiguration als Service des DI-Containers hinzu:

services:
	- MyAuthenticator

Events $onLoggedIn, $onLoggedOut

Das Objekt Nette\Security\User hat die Events $onLoggedIn und $onLoggedOut, Sie können also Callbacks hinzufügen, die nach einer erfolgreichen Anmeldung bzw. nach dem Abmelden des Benutzers ausgelöst werden.

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

Identity

Eine Identity ist eine Menge von Informationen über einen Benutzer, die der Authenticator zurückgibt und die anschließend in der Session gespeichert wird und sich mit $user->getIdentity() abrufen lässt. So bekommen wir die ID, die Rollen und weitere Daten des Benutzers, genau so, wie wir sie im Authenticator übergeben haben:

$user->getIdentity()->getId();
// die Abkürzung $user->getId() funktioniert ebenfalls

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

// die Daten des Benutzers sind als Properties zugänglich
// der Benutzername, den wir in MyAuthenticator übergeben haben
$user->getIdentity()->name;

Wichtig ist, dass beim Abmelden mit $user->logout() die Identity nicht gelöscht wird und weiterhin verfügbar bleibt. Auch wenn ein Benutzer also eine Identity hat, muss er nicht angemeldet sein. Wollen wir die Identity ausdrücklich löschen, melden wir den Benutzer mit dem Aufruf logout(true) ab.

Dadurch können Sie weiterhin annehmen, welcher Benutzer am Rechner sitzt, und zum Beispiel in einem E-Shop personalisierte Angebote anzeigen, seine persönlichen Informationen dürfen Sie aber erst nach der Anmeldung anzeigen.

Außer dem Löschen der Identity im Einzelfall mit logout(true) können Sie ihr Erhalten über die Property $persistIdentity vollständig abschalten. Ist sie auf false gesetzt, wird die Identity bei jedem Abmelden und beim Ablauf verworfen, sodass getIdentity() dann null zurückgibt. Ob die Identity erhalten bleibt, hängt außerdem vom Speicher ab: Der Cookie-Speicher kann sie nach dem Abmelden nicht behalten, weil er das Cookie immer löscht.

Eine Identity 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; ändern wir also zum Beispiel die Rolle eines der angemeldeten Benutzer, bleiben die alten Daten in seiner Identity, bis er sich erneut anmeldet.

Speicher für den angemeldeten Benutzer

Die beiden grundlegenden Informationen über den Benutzer, nämlich ob er angemeldet ist und seine Identity, werden üblicherweise in der Session übertragen. Das lässt sich ändern. Für das Speichern dieser Informationen ist ein Objekt zuständig, das das Interface Nette\Security\UserStorage implementiert. Zwei Standardimplementierungen stehen zur Verfügung: Nette\Bridges\SecurityHttp\SessionStorage, das die Daten in der Session überträgt, und CookieStorage, das die Daten in einem Cookie überträgt. Den Speicher wählen und konfigurieren Sie sehr bequem in der Konfiguration security › authentication.

Darüber hinaus können Sie beeinflussen, wie genau das Speichern (sleep) und Wiederherstellen (wakeup) der Identity abläuft. Nötig ist nur, dass der Authenticator das Interface Nette\Security\IdentityHandler implementiert. Die Methode sleepIdentity() wird aufgerufen, bevor die Identity in den Speicher geschrieben wird, und wakeupIdentity() nach dem Lesen. Diese Methoden können den Inhalt der Identity verändern oder sie durch ein neues Objekt ersetzen, das sie zurückgeben. Die Methode wakeupIdentity() kann sogar null zurückgeben, was den Benutzer abmeldet. Das Interface deklariert außerdem die Methode getGuestIdentity(), siehe Gast-Identity.

Zeigen wir als Beispiel die Lösung der häufigen Frage, wie man die Rollen in der Identity gleich nach dem Laden aus der Session aktualisiert. In der Methode wakeupIdentity() übergeben wir die aktuellen Rollen, etwa aus einer Datenbank, in die Identity:

final class Authenticator implements
	Nette\Security\Authenticator, Nette\Security\IdentityHandler
{
	public function sleepIdentity(IIdentity $identity): IIdentity
	{
		// hier können Sie die Identity vor dem Schreiben in den Speicher nach der Anmeldung verändern,
		// aber das brauchen wir jetzt nicht
		return $identity;
	}

	public function wakeupIdentity(IIdentity $identity): ?IIdentity
	{
		// die Rollen in der Identity aktualisieren
		$userId = $identity->getId();
		$identity->setRoles($this->facade->getUserRoles($userId));
		return $identity;
	}

	public function getGuestIdentity(): ?IIdentity
	{
		// hier wird keine Gast-Identity verwendet
		return null;
	}

Kehren wir nun zum Speicher auf Basis von Cookies zurück. Er erlaubt es Ihnen, eine Website zu erstellen, auf der sich Benutzer anmelden können, ohne dass Sessions nötig sind. Sie muss also nicht auf die Festplatte schreiben. So funktioniert die Website, die Sie gerade lesen, samt Forum. In diesem Fall ist die Implementierung von IdentityHandler eine Notwendigkeit. Wir speichern im Cookie nur ein zufälliges Token, das den angemeldeten Benutzer repräsentiert.

Setzen Sie zuerst in der Konfiguration den gewünschten Speicher über security › authentication › storage: cookie.

Legen Sie in der Datenbank die Spalte authtoken an, in der jeder Benutzer einen völlig zufälligen, eindeutigen und nicht erratbaren String ausreichender Länge hat (mindestens 13 Zeichen). Der CookieStorage überträgt im Cookie nur den Wert $identity->getId(), deshalb ersetzen wir in sleepIdentity() die ursprüngliche Identity durch eine Proxy-Identity, die das authtoken in der ID enthält. Umgekehrt lesen wir in der Methode wakeupIdentity() die gesamte Identity anhand des authtoken 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 prüfen
		// ...
		// die Identity mit allen Daten aus der Datenbank zurückgeben
		return new SimpleIdentity($row->id, null, (array) $row);
	}

	public function sleepIdentity(IIdentity $identity): SimpleIdentity
	{
		// eine Proxy-Identity zurückgeben, deren ID das authtoken enthält
		return new SimpleIdentity($identity->authtoken);
	}

	public function wakeupIdentity(IIdentity $identity): ?SimpleIdentity
	{
		// die Proxy-Identity durch die vollständige Identity 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;
	}

	public function getGuestIdentity(): ?IIdentity
	{
		// hier wird keine Gast-Identity verwendet
		return null;
	}
}

Gast-Identity

Manchmal ist es praktisch, wenn auch nicht angemeldete Besucher eine Identity haben, um ihnen zum Beispiel einen Standardsatz an Rollen oder einige Daten mitzugeben. Implementiert der Authenticator IdentityHandler, kann er sie über die Methode getGuestIdentity() liefern, die immer dann verwendet wird, wenn niemand angemeldet ist. getIdentity(), getId() und getRoles() greifen dann darauf zurück, sodass Gäste eigene Rollen statt nur der schlichten Rolle guest haben können. Geben Sie null zurück, wenn Sie keine Gast-Identity wollen.

public function getGuestIdentity(): ?IIdentity
{
	return new SimpleIdentity('guest', ['guest'], ['name' => 'Guest']);
}

Die Gast-Identity wird niemals im Speicher abgelegt, und eine Anmeldung ersetzt sie immer.

Mehrere unabhängige Anmeldungen

Es ist möglich, innerhalb einer Website und einer Session gleichzeitig mehrere unabhängige Benutzer anzumelden. Wollen wir zum Beispiel für die Administration und den öffentlichen Teil der Website getrennte Authentifizierungen haben, müssen wir für jede nur einen eindeutigen Namensraum setzen:

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

Wichtig ist, daran zu denken, den Namensraum immer an allen Stellen zu setzen, die zum jeweiligen Teil gehören. Verwenden wir Presenter, setzen wir den Namensraum im gemeinsamen Vorfahren dieses Teils – üblicherweise im BasePresenter. Wir tun das, indem wir die Methode checkRequirements() erweitern:

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

Wenn Sie den Namensraum während eines einzelnen Requests wechseln (nachdem der Anmeldezustand bereits gelesen wurde), hält das Objekt User weiterhin den aus dem vorigen Namensraum gecachten Zustand. Rufen Sie in diesem Fall refreshStorage() auf, um den Cache zu verwerfen und ein erneutes Laden aus dem neuen Namensraum zu erzwingen:

$user->getStorage()->setNamespace('admin');
$user->refreshStorage(); // den Zustand aus dem neuen Namensraum neu laden

Mehrere Authenticatoren

Die Aufteilung einer Anwendung in Teile mit unabhängiger Anmeldung erfordert üblicherweise auch verschiedene Authenticatoren. Würden wir jedoch zwei Klassen, die Authenticator implementieren, in der Servicekonfiguration registrieren, wüsste Nette nicht, welche es dem Objekt Nette\Security\User automatisch zuweisen soll, und würde einen Fehler anzeigen. Deshalb müssen wir das Autowiring für die Authenticatoren so einschränken, dass es nur greift, wenn jemand eine bestimmte Klasse anfordert, etwa FrontAuthenticator. Das erreichen wir mit der Wahl autowired: self:

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

Den Authenticator des User-Objekts setzen wir vor dem Aufruf der Methode login(), also üblicherweise 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.x