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);
// ...
};